diff --git a/.gitignore b/.gitignore index ea21f0f1..584f2066 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ coverage.xml # Django stuff: *.log +*.log.zip local_settings.py *.sqlite3 *.sqlite3-journal diff --git a/chats/serializers.py b/chats/serializers.py index 1600cd92..c4fb4185 100644 --- a/chats/serializers.py +++ b/chats/serializers.py @@ -12,11 +12,11 @@ class DirectChatListSerializer(serializers.ModelSerializer): last_message = serializers.SerializerMethodField() - users = serializers.SerializerMethodField(read_only=True) + opponent = serializers.SerializerMethodField() - @classmethod - def get_users(cls, chat: ProjectChat): - return UserListSerializer(chat.get_users(), many=True).data + def get_opponent(self, chat: DirectChat): + user = self.context.get("opponent") + return UserDetailSerializer(user).data @classmethod def get_last_message(cls, chat: DirectChat): @@ -26,23 +26,23 @@ class Meta: model = DirectChat fields = [ "id", - "users", + "opponent", "last_message", ] class DirectChatDetailSerializer(serializers.ModelSerializer): - users = serializers.SerializerMethodField(read_only=True) + opponent = serializers.SerializerMethodField() - @classmethod - def get_users(cls, chat: ProjectChat): - return UserListSerializer(chat.get_users(), many=True).data + def get_opponent(self, chat: DirectChat): + user = self.context.get("opponent") + return UserDetailSerializer(user).data class Meta: model = DirectChat fields = [ "id", - "users", + "opponent", ] diff --git a/chats/views.py b/chats/views.py index 4470c822..9850230d 100644 --- a/chats/views.py +++ b/chats/views.py @@ -36,6 +36,31 @@ def get_queryset(self): user = self.request.user return user.direct_chats.all() + def get(self, request, *args, **kwargs): + chats = self.get_queryset() + serialized_chats = [] + for chat in chats: + # fixme: move to function like get_user() and get_opponent() + chat_id = chat.id + user1_id, user2_id = map(int, chat_id.split("_")) + + try: + user1 = User.objects.get(pk=user1_id) + user2 = User.objects.get(pk=user2_id) + except User.DoesNotExist: + # fixme: show deleted profile + continue + + if user1 == request.user: + opponent = user2 + else: # fixme: if user1 == user2 + opponent = user1 + + context = {"opponent": opponent} + serialized_chat = DirectChatListSerializer(chat, context=context).data + serialized_chats.append(serialized_chat) + return Response(serialized_chats, status=status.HTTP_200_OK) + class ProjectChatList(ListAPIView): serializer_class = ProjectChatListSerializer @@ -70,16 +95,17 @@ def get(self, request, *args, **kwargs) -> Response: user1 = User.objects.get(pk=user1_id) user2 = User.objects.get(pk=user2_id) - data = DirectChatDetailSerializer(DirectChat.get_chat(user1, user2)).data - if user1 == request.user: - # may be is better to use serializer or return dict - - # {"first_name": user2.first_name, "last_name": user2.last_name} - data["name"] = f"{user2.first_name} {user2.last_name}" - data["image_address"] = user2.avatar + opponent = user2 else: - data["name"] = f"{user1.first_name} {user1.last_name}" - data["image_address"] = user1.avatar + opponent = user1 + context = {"opponent": opponent} + data = DirectChatDetailSerializer( + DirectChat.get_chat(user1, user2), context=context + ).data + + data["name"] = f"{opponent.first_name} {opponent.last_name}" + data["image_address"] = opponent.avatar return Response( status=status.HTTP_200_OK, diff --git a/core/log/middleware.py b/core/log/middleware.py new file mode 100644 index 00000000..55462ef9 --- /dev/null +++ b/core/log/middleware.py @@ -0,0 +1,35 @@ +from loguru import logger +from django.conf import settings +import logging +from core.log.utils import InterceptHandler + + +class CustomLoguruMiddleware: + def __init__(self, get_response): + self.get_response = get_response + logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True) + + if settings.DEBUG: + logger.add( + f"{settings.BASE_DIR}/log/debug.log", + level="DEBUG", + **settings.LOGURU_LOGGING, + ) + logger.add( + f"{settings.BASE_DIR}/log/info.log", + level="INFO", + **settings.LOGURU_LOGGING, + ) + logger.add( + f"{settings.BASE_DIR}/log/warning.log", + level="WARNING", + **settings.LOGURU_LOGGING, + ) + + def __call__(self, request): + response = self.get_response(request) + logger.info(f"{request.method} {request.get_full_path()}") + return response + + def process_exception(self, request, exception): + logger.warning(f"{exception} http_path={request.get_full_path()}") diff --git a/core/log/utils.py b/core/log/utils.py new file mode 100644 index 00000000..2b9fee47 --- /dev/null +++ b/core/log/utils.py @@ -0,0 +1,18 @@ +import logging +import sys +from loguru import logger + + +class InterceptHandler(logging.Handler): + def emit(self, record): + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + frame, depth = sys._getframe(6), 6 + while frame and frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) diff --git a/docker-compose.dev-ci.yml b/docker-compose.dev-ci.yml index 5f661a92..b6cbc389 100644 --- a/docker-compose.dev-ci.yml +++ b/docker-compose.dev-ci.yml @@ -1,19 +1,60 @@ -version: "3.4" +version: '3.9' + services: - server: - ports: - - "8000:8000" + web: + container_name: web build: context: . - dockerfile: Dockerfile + dockerfile: ./Dockerfile + image: ghcr.io/procollab-github/api:latest + restart: always + volumes: + - log:/procollab/log env_file: - .env - restart: always - networks: - template-network: - -networks: - template-network: - + environment: + HOST: 0.0.0.0 + grafana: + container_name: grafana + image: grafana/grafana:latest + ports: + - "3000" + volumes: + - grafana-data:/var/lib/grafana + - grafana-configs:/etc/grafana + environment: + - GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s:%(http_port)s/grafana + - GF_SERVER_SERVE_FROM_SUB_PATH=true + prometheus: + container_name: prometheus + image: prom/prometheus:v2.36.0 + ports: + - "9090" + volumes: + - prom-data:/prometheus + - ./prometheus:/etc/prometheus + node-exporter: + container_name: node-exporter + image: prom/node-exporter:v1.3.1 + ports: + - "9100" + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude' + - '^/(sys|proc|dev|host|etc|rootfs/var/lib/docker/containers|rootfs/var/lib/docker/overlay2|rootfs/run/docker/netns|rootfs/var/lib/docker/aufs)($$|/)' + nginx: + container_name: nginx + build: ./nginx + ports: + - 8000:80 volumes: - db-volume: \ No newline at end of file + grafana-data: + grafana-configs: + prom-data: + prom-configs: + log: \ No newline at end of file diff --git a/docker-compose.prod-ci.yml b/docker-compose.prod-ci.yml index cb8348fb..b6cbc389 100644 --- a/docker-compose.prod-ci.yml +++ b/docker-compose.prod-ci.yml @@ -1,17 +1,60 @@ -version: "3.4" +version: '3.9' + services: - server: - ports: - - "8000:8000" + web: + container_name: web + build: + context: . + dockerfile: ./Dockerfile image: ghcr.io/procollab-github/api:latest + restart: always + volumes: + - log:/procollab/log env_file: - .env - restart: always - networks: - template-network: - -networks: - template-network: - + environment: + HOST: 0.0.0.0 + grafana: + container_name: grafana + image: grafana/grafana:latest + ports: + - "3000" + volumes: + - grafana-data:/var/lib/grafana + - grafana-configs:/etc/grafana + environment: + - GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s:%(http_port)s/grafana + - GF_SERVER_SERVE_FROM_SUB_PATH=true + prometheus: + container_name: prometheus + image: prom/prometheus:v2.36.0 + ports: + - "9090" + volumes: + - prom-data:/prometheus + - ./prometheus:/etc/prometheus + node-exporter: + container_name: node-exporter + image: prom/node-exporter:v1.3.1 + ports: + - "9100" + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude' + - '^/(sys|proc|dev|host|etc|rootfs/var/lib/docker/containers|rootfs/var/lib/docker/overlay2|rootfs/run/docker/netns|rootfs/var/lib/docker/aufs)($$|/)' + nginx: + container_name: nginx + build: ./nginx + ports: + - 8000:80 volumes: - db-volume: \ No newline at end of file + grafana-data: + grafana-configs: + prom-data: + prom-configs: + log: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ee6384ae..b6cbc389 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,14 +2,59 @@ version: '3.9' services: web: + container_name: web build: context: . dockerfile: ./Dockerfile - ports: - - "8000:8000" image: ghcr.io/procollab-github/api:latest restart: always + volumes: + - log:/procollab/log env_file: - .env environment: - HOST: 0.0.0.0 \ No newline at end of file + HOST: 0.0.0.0 + grafana: + container_name: grafana + image: grafana/grafana:latest + ports: + - "3000" + volumes: + - grafana-data:/var/lib/grafana + - grafana-configs:/etc/grafana + environment: + - GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s:%(http_port)s/grafana + - GF_SERVER_SERVE_FROM_SUB_PATH=true + prometheus: + container_name: prometheus + image: prom/prometheus:v2.36.0 + ports: + - "9090" + volumes: + - prom-data:/prometheus + - ./prometheus:/etc/prometheus + node-exporter: + container_name: node-exporter + image: prom/node-exporter:v1.3.1 + ports: + - "9100" + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude' + - '^/(sys|proc|dev|host|etc|rootfs/var/lib/docker/containers|rootfs/var/lib/docker/overlay2|rootfs/run/docker/netns|rootfs/var/lib/docker/aufs)($$|/)' + nginx: + container_name: nginx + build: ./nginx + ports: + - 8000:80 +volumes: + grafana-data: + grafana-configs: + prom-data: + prom-configs: + log: \ No newline at end of file diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 00000000..29ffd3c9 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.21-alpine + +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/conf.d \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 00000000..e6e2390c --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,19 @@ +server { + + listen 80; + + server_name api.procollab.ru; + client_max_body_size 100M; + + location / { + proxy_pass http://web:8000; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + } + + location /grafana { + proxy_pass http://grafana:3000; + proxy_set_header Host $host; + } +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 8279186f..a032e83c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -718,6 +718,20 @@ files = [ [package.dependencies] Django = ">=3.2" +[[package]] +name = "django-prometheus" +version = "2.3.1" +description = "Django middlewares to monitor your application with Prometheus.io." +optional = false +python-versions = "*" +files = [ + {file = "django-prometheus-2.3.1.tar.gz", hash = "sha256:f9c8b6c780c9419ea01043c63a437d79db2c33353451347894408184ad9c3e1e"}, + {file = "django_prometheus-2.3.1-py2.py3-none-any.whl", hash = "sha256:cf9b26f7ba2e4568f08f8f91480a2882023f5908579681bcf06a4d2465f12168"}, +] + +[package.dependencies] +prometheus-client = ">=0.7" + [[package]] name = "django-rest-passwordreset" version = "1.3.0" @@ -1001,6 +1015,24 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "loguru" +version = "0.7.1" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.1-py3-none-any.whl", hash = "sha256:046bf970cb3cad77a28d607cbf042ac25a407db987a1e801c7f7e692469982f9"}, + {file = "loguru-0.7.1.tar.gz", hash = "sha256:7ba2a7d81b79a412b0ded69bd921e012335e80fd39937a633570f273a343579e"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "pre-commit (==3.3.1)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + [[package]] name = "markupsafe" version = "2.1.3" @@ -1028,6 +1060,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1419,6 +1461,20 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "prometheus-client" +version = "0.17.1" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.6" +files = [ + {file = "prometheus_client-0.17.1-py3-none-any.whl", hash = "sha256:e537f37160f6807b8202a6fc4764cdd19bac5480ddd3e0d463c3002b34462101"}, + {file = "prometheus_client-0.17.1.tar.gz", hash = "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091"}, +] + +[package.extras] +twisted = ["twisted"] + [[package]] name = "psycopg2-binary" version = "2.9.6" @@ -1995,6 +2051,20 @@ files = [ [package.extras] brotli = ["Brotli"] +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + [[package]] name = "yarl" version = "1.9.2" @@ -2132,4 +2202,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "0f76fb18c01b57141680a43f056e6c0d1491159a5f9e923df9bdf9b6b00bff69" +content-hash = "29f00da9ad9d238fd7ba5e0d8adb8c2f729e233245ec1c46dc14a6483bf159bb" diff --git a/procollab/settings.py b/procollab/settings.py index 5e6663e5..6439c4d8 100644 --- a/procollab/settings.py +++ b/procollab/settings.py @@ -6,6 +6,7 @@ from decouple import config from sentry_sdk.integrations.django import DjangoIntegration + mimetypes.add_type("application/javascript", ".js", True) mimetypes.add_type("text/css", ".css", True) mimetypes.add_type("text/html", ".html", True) @@ -26,12 +27,14 @@ TAGGIT_CASE_INSENSITIVE = True CSRF_TRUSTED_ORIGINS = [ - "http://localhost", + "http://localhost:8000", + "http://127.0.0.1:8000", + "http://0.0.0.0:8000", "https://api.procollab.ru", "https://procollab.ru", "https://www.procollab.ru", "https://app.procollab.ru", - "https://dev.procollab.ru" + "https://dev.procollab.ru", ] ALLOWED_HOSTS = [ @@ -41,8 +44,10 @@ "0.0.0.0", "api.procollab.ru", "app.procollab.ru", + "dev.procollab.ru", "procollab.ru", - "dev.procollab.ru" + "dev.procollab.ru", + "web", # From Docker ] PASSWORD_HASHERS = [ @@ -101,9 +106,11 @@ "drf_yasg", "channels", "taggit", + "django_prometheus", ] MIDDLEWARE = [ + "django_prometheus.middleware.PrometheusBeforeMiddleware", "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -114,8 +121,11 @@ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "debug_toolbar.middleware.DebugToolbarMiddleware", + "django_prometheus.middleware.PrometheusAfterMiddleware", + "core.log.middleware.CustomLoguruMiddleware", ] + # CORS_ALLOWED_ORIGINS = [ # "http://localhost:4200", # "http://127.0.0.1:4200", @@ -171,14 +181,15 @@ if DEBUG: DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", + "ENGINE": "django_prometheus.db.backends.sqlite3", "NAME": "db.sqlite3", } } CACHES = { "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "BACKEND": "django_prometheus.cache.backends.filebased.FileBasedCache", + "LOCATION": "/var/tmp/django_cache", } } @@ -217,7 +228,7 @@ DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql", + "ENGINE": "django_prometheus.db.backends.postgresql", "NAME": config("DATABASE_NAME", default="postgres", cast=str), "USER": config("DATABASE_USER", default="postgres", cast=str), "PASSWORD": config("DATABASE_PASSWORD", default="postgres", cast=str), @@ -320,5 +331,34 @@ SELECTEL_SWIFT_URL = ( f"https://api.selcdn.ru/v1/SEL_{SELECTEL_ACCOUNT_ID}/{SELECTEL_CONTAINER_NAME}/" ) + +LOGURU_LOGGING = { + "rotation": "300 MB", + "compression": "zip", + "retention": "10 days", + "enqueue": True, +} + + if DEBUG: SELECTEL_SWIFT_URL += "debug/" + +PROMETHEUS_LATENCY_BUCKETS = ( + 0.01, + 0.025, + 0.05, + 0.075, + 0.1, + 0.25, + 0.5, + 0.75, + 1.0, + 2.5, + 5.0, + 7.5, + 10.0, + 25.0, + 50.0, + 75.0, + float("inf"), +) diff --git a/procollab/urls.py b/procollab/urls.py index afdd895d..4da47be5 100644 --- a/procollab/urls.py +++ b/procollab/urls.py @@ -51,6 +51,7 @@ path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"), path("", include("metrics.urls", namespace="metrics")), + path("django_prometheus/", include("django_prometheus.urls")), ] if settings.DEBUG: diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml new file mode 100644 index 00000000..35fe0993 --- /dev/null +++ b/prometheus/prometheus.yml @@ -0,0 +1,10 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: monitoring + metrics_path: /django_prometheus/metrics + static_configs: + - targets: + - web:8000 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c8607196..7c0fd0bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,8 @@ django-taggit = "^3.1.0" requests = "^2.31.0" coreapi = "^2.3.3" webp = "^0.1.6" +django-prometheus = "^2.3.1" +loguru = "^0.7.1" [build-system] diff --git a/users/filters.py b/users/filters.py index ef01c5a5..cf107fc8 100644 --- a/users/filters.py +++ b/users/filters.py @@ -1,4 +1,5 @@ from django.contrib.auth import get_user_model +from django.db.models import Q from django_filters import rest_framework as filters from partner_programs.models import PartnerProgram, PartnerProgramUserProfile @@ -42,6 +43,24 @@ def filter_by_partner_program(cls, queryset, name, value): except PartnerProgram.DoesNotExist: return User.objects.none() + @classmethod + def filter_by_fullname(cls, queryset, name, value): + words = value.split() + first_word = words[0] + if len(words) >= 2: + # if there are more than 2 words, we assume that the first two are first_name and last_name + first_word, second_word = words[0], words[1] + # we search for both first_name and last_name in both orders + return queryset.filter( + Q(first_name__icontains=first_word) + | Q(last_name__icontains=second_word) + | Q(first_name__icontains=second_word) + | Q(last_name__icontains=first_word) + ) + return queryset.filter( + Q(first_name__icontains=first_word) | Q(last_name__icontains=first_word) + ) + about_me__contains = filters.Filter(field_name="about_me", lookup_expr="contains") key_skills__contains = filters.Filter(field_name="key_skills", lookup_expr="contains") useful_to_project__contains = filters.Filter( @@ -51,6 +70,7 @@ def filter_by_partner_program(cls, queryset, name, value): partner_program = filters.NumberFilter( field_name="partner_program", method="filter_by_partner_program" ) + fullname = filters.CharFilter(method="filter_by_fullname") class Meta: model = User diff --git a/users/migrations/0039_alter_customuser_first_name_and_more.py b/users/migrations/0039_alter_customuser_first_name_and_more.py index 394709ed..e71839ed 100644 --- a/users/migrations/0039_alter_customuser_first_name_and_more.py +++ b/users/migrations/0039_alter_customuser_first_name_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.3 on 2023-08-08 19:56 +# Generated by Django 4.2.3 on 2023-07-25 20:49 from django.db import migrations, models import users.validators