diff --git a/.gitignore b/.gitignore index 0b655bd0e7..3989f43561 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ staticfiles mediafiles .env .DS_Store +logs/ node_modules/ assets/dist/ diff --git a/apiserver/.env.example b/apiserver/.env.example index 42b0e32e55..eba436691a 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -63,4 +63,3 @@ WEB_URL="http://localhost" # Gunicorn Workers GUNICORN_WORKERS=2 - diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index 146f61f48d..4e368c6976 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -1,26 +1,27 @@ # Python imports -import zoneinfo +import logging from urllib.parse import urlparse +import zoneinfo # Django imports from django.conf import settings -from django.db import IntegrityError from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.db import IntegrityError from django.utils import timezone +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response # Third party imports from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated -from rest_framework import status from sentry_sdk import capture_exception # Module imports from plane.api.middleware.api_authentication import APIKeyAuthentication from plane.api.rate_limit import ApiKeyRateThrottle -from plane.utils.paginator import BasePaginator from plane.bgtasks.webhook_task import send_webhook +from plane.utils.paginator import BasePaginator class TimezoneMixin: @@ -104,6 +105,35 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator): status=status.HTTP_400_BAD_REQUEST, ) + if isinstance(e, ValidationError): + return Response( + {"error": "Please provide valid detail"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if isinstance(e, ObjectDoesNotExist): + model_name = str(exc).split(" matching query does not exist.")[ + 0 + ] + return Response( + {"error": f"{model_name} does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + + if isinstance(e, KeyError): + return Response( + {"error": f"key {e} does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + logger = logging.getLogger("plane") + logger.error(e) + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + if isinstance(e, ValidationError): return Response( { diff --git a/apiserver/plane/bgtasks/analytic_plot_export.py b/apiserver/plane/bgtasks/analytic_plot_export.py index 62620ab9d0..04b0158f23 100644 --- a/apiserver/plane/bgtasks/analytic_plot_export.py +++ b/apiserver/plane/bgtasks/analytic_plot_export.py @@ -1,22 +1,23 @@ # Python imports import csv import io +import logging + +# Third party imports +from celery import shared_task +from django.conf import settings # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string from django.utils.html import strip_tags -from django.conf import settings - -# Third party imports -from celery import shared_task from sentry_sdk import capture_exception # Module imports from plane.db.models import Issue +from plane.license.utils.instance_value import get_email_configuration from plane.utils.analytics_plot import build_graph_plot from plane.utils.issue_filters import issue_filters -from plane.license.utils.instance_value import get_email_configuration row_mapping = { "state__name": "State", @@ -210,9 +211,9 @@ def generate_segmented_rows( None, ) if assignee: - generated_row[ - 0 - ] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" + generated_row[0] = ( + f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" + ) if x_axis == LABEL_ID: label = next( @@ -279,9 +280,9 @@ def generate_segmented_rows( None, ) if assignee: - row_zero[ - index + 2 - ] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" + row_zero[index + 2] = ( + f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" + ) if segmented == LABEL_ID: for index, segm in enumerate(row_zero[2:]): @@ -366,9 +367,9 @@ def generate_non_segmented_rows( None, ) if assignee: - row[ - 0 - ] = f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" + row[0] = ( + f"{assignee['assignees__first_name']} {assignee['assignees__last_name']}" + ) if x_axis == LABEL_ID: label = next( @@ -506,8 +507,7 @@ def analytic_export_task(email, data, slug): send_export_email(email, slug, csv_buffer, rows) return except Exception as e: - print(e) - if settings.DEBUG: - print(e) + logger = logging.getLogger("plane") + logger.error(e) capture_exception(e) return diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index f99e54215f..8ad257ea74 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -4,6 +4,8 @@ import io import json import boto3 import zipfile +import logging +from urllib.parse import urlparse, urlunparse # Django imports from django.conf import settings @@ -403,8 +405,7 @@ def issue_export_task( exporter_instance.status = "failed" exporter_instance.reason = str(e) exporter_instance.save(update_fields=["status", "reason"]) - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) + logger = logging.getLogger("plane") + logger.error(e) capture_exception(e) return diff --git a/apiserver/plane/bgtasks/forgot_password_task.py b/apiserver/plane/bgtasks/forgot_password_task.py index 1d3b684776..a1033a6584 100644 --- a/apiserver/plane/bgtasks/forgot_password_task.py +++ b/apiserver/plane/bgtasks/forgot_password_task.py @@ -1,13 +1,13 @@ -# Python import +# Python imports +import logging + +# Third party imports +from celery import shared_task # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string from django.utils.html import strip_tags -from django.conf import settings - -# Third party imports -from celery import shared_task from sentry_sdk import capture_exception # Module imports @@ -62,8 +62,7 @@ def forgot_password(first_name, email, uidb64, token, current_site): msg.send() return except Exception as e: - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) + logger = logging.getLogger("plane") + logger.error(e) capture_exception(e) return diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 6aa6b6695f..f350b8ce35 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -1,6 +1,7 @@ # Python imports import json import requests +import logging # Django imports from django.conf import settings @@ -1668,8 +1669,7 @@ def issue_activity( return except Exception as e: - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) + logger = logging.getLogger("plane") + logger.error(e) capture_exception(e) return diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index 08c07b7b35..981c3d2c1f 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -1,6 +1,7 @@ # Python imports import json from datetime import timedelta +import logging # Django imports from django.utils import timezone @@ -96,8 +97,8 @@ def archive_old_issues(): ] return except Exception as e: - if settings.DEBUG: - print(e) + logger = logging.getLogger("plane") + logger.error(e) capture_exception(e) return @@ -179,7 +180,7 @@ def close_old_issues(): ] return except Exception as e: - if settings.DEBUG: - print(e) + logger = logging.getLogger("plane") + logger.error(e) capture_exception(e) return diff --git a/apiserver/plane/bgtasks/magic_link_code_task.py b/apiserver/plane/bgtasks/magic_link_code_task.py index 019f5b13cd..adc79b5ec5 100644 --- a/apiserver/plane/bgtasks/magic_link_code_task.py +++ b/apiserver/plane/bgtasks/magic_link_code_task.py @@ -1,13 +1,14 @@ # Python imports +import logging + +# Third party imports +from celery import shared_task +from django.conf import settings # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string from django.utils.html import strip_tags -from django.conf import settings - -# Third party imports -from celery import shared_task from sentry_sdk import capture_exception # Module imports @@ -54,9 +55,7 @@ def magic_link(email, key, token, current_site): msg.send() return except Exception as e: - print(e) + logger = logging.getLogger("plane") + logger.error(e) capture_exception(e) - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) return diff --git a/apiserver/plane/bgtasks/project_invitation_task.py b/apiserver/plane/bgtasks/project_invitation_task.py index d24db5ae9c..fbc9a3d522 100644 --- a/apiserver/plane/bgtasks/project_invitation_task.py +++ b/apiserver/plane/bgtasks/project_invitation_task.py @@ -1,17 +1,17 @@ -# Python import +# Python imports +import logging + +# Third party imports +from celery import shared_task # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string from django.utils.html import strip_tags -from django.conf import settings - -# Third party imports -from celery import shared_task from sentry_sdk import capture_exception # Module imports -from plane.db.models import Project, User, ProjectMemberInvite +from plane.db.models import Project, ProjectMemberInvite, User from plane.license.utils.instance_value import get_email_configuration @@ -77,8 +77,7 @@ def project_invitation(email, project_id, token, current_site, invitor): except (Project.DoesNotExist, ProjectMemberInvite.DoesNotExist): return except Exception as e: - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) + logger = logging.getLogger("plane") + logger.error(e) capture_exception(e) return diff --git a/apiserver/plane/bgtasks/workspace_invitation_task.py b/apiserver/plane/bgtasks/workspace_invitation_task.py index cc3000bbb6..154e116837 100644 --- a/apiserver/plane/bgtasks/workspace_invitation_task.py +++ b/apiserver/plane/bgtasks/workspace_invitation_task.py @@ -1,17 +1,17 @@ # Python imports +import logging + +# Third party imports +from celery import shared_task # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string from django.utils.html import strip_tags -from django.conf import settings - -# Third party imports -from celery import shared_task from sentry_sdk import capture_exception # Module imports -from plane.db.models import Workspace, WorkspaceMemberInvite, User +from plane.db.models import User, Workspace, WorkspaceMemberInvite from plane.license.utils.instance_value import get_email_configuration @@ -82,8 +82,7 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor): print("Workspace or WorkspaceMember Invite Does not exists") return except Exception as e: - # Print logs if in DEBUG mode - if settings.DEBUG: - print(e) + logger = logging.getLogger("plane") + logger.error(e) capture_exception(e) return diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index c9a8b4cb6d..5f932d2ea9 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -1,16 +1,17 @@ # Python imports -import uuid -import string import random +import string +import uuid + import pytz +from django.contrib.auth.models import ( + AbstractBaseUser, + PermissionsMixin, + UserManager, +) # Django imports from django.db import models -from django.contrib.auth.models import ( - AbstractBaseUser, - UserManager, - PermissionsMixin, -) from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 5c8947e73b..1a63f9c509 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -3,19 +3,20 @@ # Python imports import os import ssl -import certifi from datetime import timedelta from urllib.parse import urlparse -# Django imports -from django.core.management.utils import get_random_secret_key +import certifi # Third party imports import dj_database_url import sentry_sdk + +# Django imports +from django.core.management.utils import get_random_secret_key +from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.redis import RedisIntegration -from sentry_sdk.integrations.celery import CeleryIntegration BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -345,3 +346,49 @@ INSTANCE_KEY = os.environ.get( SKIP_ENV_VAR = os.environ.get("SKIP_ENV_VAR", "1") == "1" DATA_UPLOAD_MAX_MEMORY_SIZE = int(os.environ.get("FILE_SIZE_LIMIT", 5242880)) + +LOG_DIR = os.path.join(BASE_DIR, "logs") + +if not os.path.exists(LOG_DIR): + os.makedirs(LOG_DIR) + + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", + "style": "{", + }, + "json": { + "()": "pythonjsonlogger.jsonlogger.JsonFormatter", + "fmt": "%(levelname)s %(asctime)s %(module)s %(name)s %(message)s", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + "file": { + "class": "logging.handlers.TimedRotatingFileHandler", + "filename": ( + os.path.join(BASE_DIR, "logs", "debug.log") + if DEBUG + else os.path.join(BASE_DIR, "logs", "error.log") + ), + "when": "midnight", + "interval": 1, # One day + "backupCount": 5, # Keep last 5 days of logs, + "formatter": "json", + }, + }, + "loggers": { + "plane": { + "level": "DEBUG" if DEBUG else "ERROR", + "handlers": ["console", "file"], + "propagate": False, + }, + }, +} diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml index b4be0e2dc1..a6f3802519 100644 --- a/deploy/selfhost/docker-compose.yml +++ b/deploy/selfhost/docker-compose.yml @@ -90,6 +90,8 @@ services: command: ./bin/takeoff deploy: replicas: ${API_REPLICAS:-1} + volumes: + - logs/api:/code/plane/logs depends_on: - plane-db - plane-redis @@ -100,6 +102,8 @@ services: pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: ./bin/worker + volumes: + - logs/worker:/code/plane/logs depends_on: - api - plane-db @@ -111,6 +115,8 @@ services: pull_policy: ${PULL_POLICY:-always} restart: unless-stopped command: ./bin/beat + volumes: + - logs/beat-worker:/code/plane/logs depends_on: - api - plane-db @@ -169,3 +175,4 @@ volumes: pgdata: redisdata: uploads: + logs: