diff --git a/Dockerfile b/Dockerfile index a40f4c2..a8fcb2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,4 +36,4 @@ COPY . /app WORKDIR /app -CMD ["uvicorn", "src.asgi:app", "--reload", "--reload-dir=/app", "--port=8000", "--host=0.0.0.0", "--log-level=debug"] +CMD [ "gunicorn", "src.asgi:app", "--config=/app/config/gunicorn.py" ] diff --git a/config/gunicorn.py b/config/gunicorn.py new file mode 100644 index 0000000..dc8bf52 --- /dev/null +++ b/config/gunicorn.py @@ -0,0 +1,18 @@ +import os + +# https://docs.gunicorn.org/en/stable/settings.html#settings + +workers = os.cpu_count() +bind = "0.0.0.0:8000" +worker_class = "uvicorn.workers.UvicornWorker" +worker_connections = 1000 # default +threads = 1 # default + +# restart workeru +max_requests = 500000 +max_requests_jitter = 10000 + +timeout = 30 # default +graceful_timeout = 30 # default + +keepalive = 600 # s klientem udrzuje nginx diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 22c67b7..adbca0c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -3,7 +3,6 @@ version: "3.3" services: api: image: "docker.kvacek.cz/poletani/api:latest" - command: "uvicorn src.asgi:app --port=8000 --host=0.0.0.0 --workers 4" network_mode: "host" restart: "always" env_file: diff --git a/requirements.txt b/requirements.txt index 471270a..f45e55e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,10 @@ fastapi-jwt[authlib]==0.3.0 bcrypt fastapi-mail==1.4.2 strawberry-graphql[fastapi]==0.258.0 -uvicorn==0.34.0 + +gunicorn == 23.0.0 +uvicorn[standard] == 0.34.0 + sqlalchemy[asyncio] == 2.0.37 aiomysql==0.2.0 alembic==1.14.1 @@ -15,3 +18,5 @@ exif==1.6.1 aiocache==0.12.3 aiohttp==3.11.11 lxml==5.3.0 + +colorlog diff --git a/src/graphql_schema/context.py b/src/graphql_schema/context.py index 746abb4..386df77 100644 --- a/src/graphql_schema/context.py +++ b/src/graphql_schema/context.py @@ -1,4 +1,5 @@ import dataclasses +from datetime import datetime from fastapi import BackgroundTasks, Depends, Security from fastapi_jwt import JwtAuthorizationCredentials from sqlalchemy import select @@ -9,6 +10,7 @@ from jwt import access_security @dataclasses.dataclass class GraphQLContext(BaseContext): + request_start_time: datetime user_id: int organization_ids: set[int] jwt_auth_credentials: JwtAuthorizationCredentials @@ -29,5 +31,6 @@ async def setup_graphql_context(credentials: JwtAuthorizationCredentials = Secur user_id=user_id, organization_ids=organization_ids, jwt_auth_credentials=credentials, - background_tasks=Depends(BackgroundTasks) + background_tasks=Depends(BackgroundTasks), + request_start_time=datetime.now() ) diff --git a/src/graphql_schema/schema.py b/src/graphql_schema/schema.py index 912abec..ab03e48 100644 --- a/src/graphql_schema/schema.py +++ b/src/graphql_schema/schema.py @@ -1,37 +1,30 @@ +from datetime import datetime import strawberry -from strawberry.extensions import SchemaExtension, ValidationCache - +from strawberry.extensions import SchemaExtension, ValidationCache, ParserCache from graphql_schema.extensions.schema.error_logging import ErrorLogging +from logger import log from .mutation import Mutation from .query import Query -# Toto se da kdyztak pouzit jako extension do Schema -# class SQLAlchemySession(Extension): -# def on_request_start(self): -# session = async_session() -# print(self.execution_context.context) -# self.execution_context.context["db"] = session -# -# async def on_request_end(self): -# await self.execution_context.context["db"].close() - class LoggingExtension(SchemaExtension): - def on_request_start(self): - print("request start") - - async def on_request_end(self): - print("request end") - + def on_request_end(self): + # nastavuje se pri vytvareni GraphQL contextu + request_start = self.execution_context.context.request_start_time + request_end = datetime.now() + request_time_ms = round((request_end - request_start).microseconds / 1000) + op_name = self.execution_context.operation_name + log.info(f"Operation {op_name} took {request_time_ms}ms") schema = strawberry.Schema( query=Query, mutation=Mutation, extensions=[ - ErrorLogging(), LoggingExtension(), + ErrorLogging(), + ParserCache(), ValidationCache() ], ) diff --git a/src/logger.py b/src/logger.py index f4bd5bc..be83b28 100644 --- a/src/logger.py +++ b/src/logger.py @@ -1,22 +1,28 @@ import logging import sys +from colorlog import ColoredFormatter + log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -# Create handlers for logging to the standard output and a file stdoutHandler = logging.StreamHandler(stream=sys.stdout) - -# Set the log levels on the handlers stdoutHandler.setLevel(logging.DEBUG) -# Create a log format using Log Record attributes -fmt = logging.Formatter( - "%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s" + +fmt = ColoredFormatter( + "%(log_color)s[%(levelname)s]%(reset)s %(blue)s%(message)s%(reset)s %(light_black)s[%(filename)s:%(lineno)d]]%(reset)s", + datefmt=None, + reset=True, + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red,bg_white', + }, + secondary_log_colors={}, + style='%' ) - -# Set the log format on each handler stdoutHandler.setFormatter(fmt) - -# Add each handler to the Logger object log.addHandler(stdoutHandler) diff --git a/src/main.py b/src/main.py index 1badabd..f6bda60 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,9 @@ import sentry_sdk from fastapi import FastAPI, HTTPException, APIRouter from graphql import GraphQLError +from sentry_sdk.integrations.fastapi import FastApiIntegration +from sentry_sdk.integrations.starlette import StarletteIntegration +from sentry_sdk.integrations.strawberry import StrawberryIntegration from sqlalchemy.exc import NoResultFound from starlette.background import BackgroundTasks from starlette.middleware.cors import CORSMiddleware @@ -27,10 +30,23 @@ class App: def create_app(self): if SENTRY_DSN: + fast_api_params = dict( + transaction_style="endpoint", + failed_request_status_codes={403, *range(500, 599)}, + http_methods_to_capture=("GET", "POST"), + ) + sentry_sdk.init( dsn=SENTRY_DSN, enable_tracing=True, - ignore_errors=[GraphQLError, HTTPException] + traces_sample_rate=0.5, + profiles_sample_rate=0.5, + ignore_errors=[GraphQLError, HTTPException], + integrations=[ + StrawberryIntegration(async_execution=True), + StarletteIntegration(**fast_api_params), + FastApiIntegration(**fast_api_params), + ] ) app = FastAPI(