From c50283b55ebd42a55217a58915bf94e3a20f52b0 Mon Sep 17 00:00:00 2001 From: Rui Alves Date: Sat, 7 Sep 2024 16:45:39 +0100 Subject: [PATCH] POC: Added FastAPI with one endpoint (get /logs/service) --- .../rootfs/usr/local/nginx/conf/nginx.conf | 18 ++++--- frigate/api/app.py | 48 ++++++++++------- frigate/api/defs/tags.py | 7 +++ frigate/api/fastapi_app.py | 53 +++++++++++++++++++ frigate/app.py | 22 +++++++- 5 files changed, 120 insertions(+), 28 deletions(-) create mode 100644 frigate/api/defs/tags.py create mode 100644 frigate/api/fastapi_app.py diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index 186b7037c..777b75c24 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -2,7 +2,7 @@ daemon off; user root; worker_processes auto; -error_log /dev/stdout warn; +error_log /dev/stdout debug; pid /var/run/nginx.pid; events { @@ -222,12 +222,16 @@ http { include proxy.conf; } - location ~* /api/.*\.(jpg|jpeg|png|webp|gif)$ { - include auth_request.conf; - rewrite ^/api/(.*)$ $1 break; - proxy_pass http://frigate_api; - include proxy.conf; - } +# FIXME: Needed to disabled this rule, otherwise it fails for endpoints that end with one of those file extensions +# 1. with httptools it passes the auth.conf but then throws a 400 error "WARN "Invalid HTTP request received." -> https://github.com/encode/uvicorn/blob/47304d9ae76321f0f5f649ff4f73e09b17085933/uvicorn/protocols/http/httptools_impl.py#L165 +# 2. With h11 it goes through the auth.conf but returns a 404 error +# We might need to add extra rules that will allow endpoint that end with an extension OR find a fix without creating other rules +# location ~* /api/.*\.(jpg|jpeg|png|webp|gif)$ { +# include auth_request.conf; +# rewrite ^/api/(.*)$ $1 break; +# proxy_pass http://frigate_api; +# include proxy.conf; +# } location /api/ { include auth_request.conf; diff --git a/frigate/api/app.py b/frigate/api/app.py index 46a2b9c13..e48196721 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -10,6 +10,9 @@ from functools import reduce from typing import Optional import requests +from fastapi import APIRouter, Path +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse from flask import Blueprint, Flask, current_app, jsonify, make_response, request from markupsafe import escape from peewee import operator @@ -17,6 +20,7 @@ from playhouse.sqliteq import SqliteQueueDatabase from werkzeug.middleware.proxy_fix import ProxyFix from frigate.api.auth import AuthBp, get_jwt_secret, limiter +from frigate.api.defs.tags import Tags from frigate.api.event import EventBp from frigate.api.export import ExportBp from frigate.api.media import MediaBp @@ -52,6 +56,8 @@ bp.register_blueprint(ReviewBp) bp.register_blueprint(AuthBp) bp.register_blueprint(NotificationBp) +router = APIRouter() + def create_app( frigate_config, @@ -454,19 +460,26 @@ def vainfo(): ) -@bp.route("/logs/", methods=["GET"]) -def logs(service: str): +@router.get("/logs/{service}", tags=[Tags.logs]) +def logs( + service: str = Path(enum=["frigate", "nginx", "go2rtc", "chroma"]), + download: Optional[str] = None, + start: Optional[int] = 0, + end: Optional[int] = None, +): + """Get logs for the requested service (frigate/nginx/go2rtc/chroma)""" + def download_logs(service_location: str): try: file = open(service_location, "r") contents = file.read() file.close() - return jsonify(contents) + return JSONResponse(jsonable_encoder(contents)) except FileNotFoundError as e: logger.error(e) - return make_response( - jsonify({"success": False, "message": "Could not find log file"}), - 500, + return JSONResponse( + content={"success": False, "message": "Could not find log file"}, + status_code=500, ) log_locations = { @@ -478,17 +491,14 @@ def logs(service: str): service_location = log_locations.get(service) if not service_location: - return make_response( - jsonify({"success": False, "message": "Not a valid service"}), - 404, + return JSONResponse( + content={"success": False, "message": "Not a valid service"}, + status_code=404, ) - if request.args.get("download", type=bool, default=False): + if download: return download_logs(service_location) - start = request.args.get("start", type=int, default=0) - end = request.args.get("end", type=int) - try: file = open(service_location, "r") contents = file.read() @@ -529,15 +539,15 @@ def logs(service: str): logLines.append(currentLine) - return make_response( - jsonify({"totalLines": len(logLines), "lines": logLines[start:end]}), - 200, + return JSONResponse( + content={"totalLines": len(logLines), "lines": logLines[start:end]}, + status_code=200, ) except FileNotFoundError as e: logger.error(e) - return make_response( - jsonify({"success": False, "message": "Could not find log file"}), - 500, + return JSONResponse( + content={"success": False, "message": "Could not find log file"}, + status_code=500, ) diff --git a/frigate/api/defs/tags.py b/frigate/api/defs/tags.py new file mode 100644 index 000000000..5e1d12d24 --- /dev/null +++ b/frigate/api/defs/tags.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class Tags(Enum): + preview = "Preview" + logs = "Logs" + media = "Media" diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py new file mode 100644 index 000000000..902926488 --- /dev/null +++ b/frigate/api/fastapi_app.py @@ -0,0 +1,53 @@ +import logging + +from fastapi import FastAPI + +from frigate.api import app as main_app +from frigate.api.defs.tags import Tags +from frigate.plus import PlusApi +from frigate.ptz.onvif import OnvifController +from frigate.stats.emitter import StatsEmitter +from frigate.storage import StorageMaintainer + +logger = logging.getLogger(__name__) + + +# https://fastapi.tiangolo.com/tutorial/metadata/#use-your-tags +tags_metadata = [ + { + "name": Tags.preview, + "description": "Preview routes", + }, + { + "name": Tags.logs, + "description": "Logs routes", + }, + { + "name": Tags.media, + "description": "Media routes", + }, +] + + +def create_fastapi_app( + frigate_config, + detected_frames_processor, + storage_maintainer: StorageMaintainer, + onvif: OnvifController, + plus_api: PlusApi, + stats_emitter: StatsEmitter, +): + logger.info("Starting FastAPI app") + app = FastAPI(debug=False, tags_metadata=tags_metadata) + # Routes + app.include_router(main_app.router) + # App Properties + app.frigate_config = frigate_config + app.detected_frames_processor = detected_frames_processor + app.storage_maintainer = storage_maintainer + app.camera_error_image = None + app.onvif = onvif + app.plus_api = plus_api + app.stats_emitter = stats_emitter + + return app diff --git a/frigate/app.py b/frigate/app.py index 68e5c872c..3271736d7 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -14,6 +14,8 @@ from types import FrameType from typing import Optional import psutil +import uvicorn +from fastapi.middleware.wsgi import WSGIMiddleware from peewee_migrate import Router from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqliteq import SqliteQueueDatabase @@ -21,6 +23,7 @@ from pydantic import ValidationError from frigate.api.app import create_app from frigate.api.auth import hash_password +from frigate.api.fastapi_app import create_fastapi_app from frigate.comms.config_updater import ConfigPublisher from frigate.comms.dispatcher import Communicator, Dispatcher from frigate.comms.inter_process import InterProcessCommunicator @@ -397,6 +400,15 @@ class FrigateApp: self.stats_emitter, ) + self.fastapi_app = create_fastapi_app( + self.config, + self.detected_frames_processor, + self.storage_maintainer, + self.onvif_controller, + self.plus_api, + self.stats_emitter, + ) + def init_onvif(self) -> None: self.onvif_controller = OnvifController(self.config, self.ptz_metrics) @@ -754,11 +766,17 @@ class FrigateApp: signal.signal(signal.SIGTERM, receiveSignal) try: - self.flask_app.run(host="127.0.0.1", port=5001, debug=False, threaded=True) + # Run the flask app inside fastapi: https://fastapi.tiangolo.com/advanced/sub-applications/ + self.fastapi_app.mount("", WSGIMiddleware(self.flask_app)) + uvicorn.run( + self.fastapi_app, + host="127.0.0.1", + port=5001, + ) except KeyboardInterrupt: pass - logger.info("Flask has exited...") + logger.info("FastAPI/Flask has exited...") self.stop()