From 4cc1b79d81f23b51d6200758e501d0595ed4902b Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 4 Oct 2024 21:35:13 +0530 Subject: [PATCH 1/3] chore: instance tracing --- apiserver/plane/license/bgtasks/tracer.py | 96 +++++++++++++++++++ .../management/commands/register_instance.py | 73 +++++--------- apiserver/plane/settings/common.py | 1 + apiserver/plane/utils/tracer.py | 26 ----- 4 files changed, 121 insertions(+), 75 deletions(-) create mode 100644 apiserver/plane/license/bgtasks/tracer.py delete mode 100644 apiserver/plane/utils/tracer.py diff --git a/apiserver/plane/license/bgtasks/tracer.py b/apiserver/plane/license/bgtasks/tracer.py new file mode 100644 index 0000000000..f350e9a45b --- /dev/null +++ b/apiserver/plane/license/bgtasks/tracer.py @@ -0,0 +1,96 @@ +# Third party imports +from celery import shared_task +from opentelemetry import trace + +# Module imports +from plane.license.models import Instance +from plane.db.models import ( + User, + Workspace, + Project, + Issue, + Module, + Cycle, + CycleIssue, + ModuleIssue, + Page, + WorkspaceMember, +) + + +@shared_task +def instance_traces(): + # Get the tracer + tracer = trace.get_tracer(__name__) + + # Check if the instance is registered + instance = Instance.objects.first() + + # If instance is None then return + if instance is None: + return + + if instance.is_telemetry_enabled: + # Instance details + with tracer.start_as_current_span("instance_details") as span: + # Count of all models + workspace_count = Workspace.objects.count() + user_count = User.objects.count() + project_count = Project.objects.count() + issue_count = Issue.objects.count() + module_count = Module.objects.count() + cycle_count = Cycle.objects.count() + cycle_issue_count = CycleIssue.objects.count() + module_issue_count = ModuleIssue.objects.count() + page_count = Page.objects.count() + + # Set span attributes + span.set_attribute("instance_id", instance.instance_id) + span.set_attribute("instance_name", instance.instance_name) + span.set_attribute("current_version", instance.current_version) + span.set_attribute("latest_version", instance.latest_version) + span.set_attribute( + "is_telemetry_enabled", instance.is_telemetry_enabled + ) + span.set_attribute("user_count", user_count) + span.set_attribute("workspace_count", workspace_count) + span.set_attribute("project_count", project_count) + span.set_attribute("issue_count", issue_count) + span.set_attribute("module_count", module_count) + span.set_attribute("cycle_count", cycle_count) + span.set_attribute("cycle_issue_count", cycle_issue_count) + span.set_attribute("module_issue_count", module_issue_count) + span.set_attribute("page_count", page_count) + + # Workspace details + for workspace in Workspace.objects.all(): + # Count of all models + project_count = Project.objects.filter(workspace=workspace).count() + issue_count = Issue.objects.filter(workspace=workspace).count() + module_count = Module.objects.filter(workspace=workspace).count() + cycle_count = Cycle.objects.filter(workspace=workspace).count() + cycle_issue_count = CycleIssue.objects.filter( + workspace=workspace + ).count() + module_issue_count = ModuleIssue.objects.filter( + workspace=workspace + ).count() + page_count = Page.objects.filter(workspace=workspace).count() + member_count = WorkspaceMember.objects.filter( + workspace=workspace + ).count() + + # Set span attributes + with tracer.start_as_current_span("workspace_details") as span: + span.set_attribute("workspace_id", str(workspace.id)) + span.set_attribute("workspace_slug", workspace.slug) + span.set_attribute("project_count", project_count) + span.set_attribute("issue_count", issue_count) + span.set_attribute("module_count", module_count) + span.set_attribute("cycle_count", cycle_count) + span.set_attribute("cycle_issue_count", cycle_issue_count) + span.set_attribute("module_issue_count", module_issue_count) + span.set_attribute("page_count", page_count) + span.set_attribute("member_count", member_count) + + return diff --git a/apiserver/plane/license/management/commands/register_instance.py b/apiserver/plane/license/management/commands/register_instance.py index 314d475200..09d13441c6 100644 --- a/apiserver/plane/license/management/commands/register_instance.py +++ b/apiserver/plane/license/management/commands/register_instance.py @@ -11,19 +11,8 @@ from django.conf import settings from plane.license.models import Instance from plane.db.models import ( User, - Workspace, - Project, - Issue, - Module, - Cycle, - CycleIssue, - ModuleIssue, - Page, ) - -from opentelemetry import trace - -tracer = trace.get_tracer(__name__) +from plane.license.bgtasks.tracer import instance_traces class Command(BaseCommand): @@ -35,16 +24,24 @@ class Command(BaseCommand): "machine_signature", type=str, help="Machine signature" ) + def read_package_json(self): + with open("package.json", "r") as file: + # Load JSON content from the file + data = json.load(file) + + payload = { + "instance_key": settings.INSTANCE_KEY, + "version": data.get("version", 0.1), + "user_count": User.objects.filter(is_bot=False).count(), + } + return payload + def handle(self, *args, **options): # Check if the instance is registered instance = Instance.objects.first() # If instance is None then register this instance if instance is None: - with open("package.json", "r") as file: - # Load JSON content from the file - data = json.load(file) - machine_signature = options.get( "machine_signature", "machine-signature" ) @@ -52,12 +49,7 @@ class Command(BaseCommand): if not machine_signature: raise CommandError("Machine signature is required") - payload = { - "instance_key": settings.INSTANCE_KEY, - "version": data.get("version", 0.1), - "machine_signature": machine_signature, - "user_count": User.objects.filter(is_bot=False).count(), - } + payload = self.read_package_json() instance = Instance.objects.create( instance_name="Plane Community Edition", @@ -74,32 +66,15 @@ class Command(BaseCommand): self.stdout.write( self.style.SUCCESS("Instance already registered") ) + payload = self.read_package_json() + # Update the instance details + instance.last_checked_at = timezone.now() + instance.user_count = payload.get("user_count", 0) + instance.current_version = payload.get("version") + instance.latest_version = payload.get("version") + instance.save() - if instance.is_telemetry_enabled: - with tracer.start_as_current_span("instance_details") as span: - workspace_count = Workspace.objects.count() - user_count = User.objects.count() - project_count = Project.objects.count() - issue_count = Issue.objects.count() - module_count = Module.objects.count() - cycle_count = Cycle.objects.count() - cycle_issue_count = CycleIssue.objects.count() - module_issue_count = ModuleIssue.objects.count() - page_count = Page.objects.count() + # Call the instance traces task + instance_traces.delay() - span.set_attribute("instance_id", instance.instance_id) - span.set_attribute("instance_name", instance.instance_name) - span.set_attribute("current_version", instance.current_version) - span.set_attribute("latest_version", instance.latest_version) - span.set_attribute( - "is_telemetry_enabled", instance.is_telemetry_enabled - ) - span.set_attribute("user_count", user_count) - span.set_attribute("workspace_count", workspace_count) - span.set_attribute("project_count", project_count) - span.set_attribute("issue_count", issue_count) - span.set_attribute("module_count", module_count) - span.set_attribute("cycle_count", cycle_count) - span.set_attribute("cycle_issue_count", cycle_issue_count) - span.set_attribute("module_issue_count", module_issue_count) - span.set_attribute("page_count", page_count) + return diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 2504cc3e36..5e1ea39a43 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -303,6 +303,7 @@ CELERY_IMPORTS = ( "plane.bgtasks.file_asset_task", "plane.bgtasks.email_notification_task", "plane.bgtasks.api_logs_task", + "plane.license.bgtasks.tracer", # management tasks "plane.bgtasks.dummy_data_task", ) diff --git a/apiserver/plane/utils/tracer.py b/apiserver/plane/utils/tracer.py deleted file mode 100644 index 831472cbfd..0000000000 --- a/apiserver/plane/utils/tracer.py +++ /dev/null @@ -1,26 +0,0 @@ -from opentelemetry import trace -from django.conf import settings -from functools import wraps - -tracer = trace.get_tracer(__name__) - - -def trace_operation(operation_name, **attributes): - def wrapper(func): - @wraps(func) - def traced_func(*args, **kwargs): - if settings.TELEMETRY_ENABLED: - with tracer.start_as_current_span(operation_name) as span: - for key, value in attributes.items(): - span.set_attribute(key, value) - result = func(*args, **kwargs) - span.add_event( - "operation_completed", {"result": str(result)} - ) - return result - else: - return func(*args, **kwargs) - - return traced_func - - return wrapper From 7bb04003ea6249fd1d20c2fe91a9d69b204f4404 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 7 Oct 2024 15:56:27 +0530 Subject: [PATCH 2/3] fix: instance trace --- apiserver/plane/license/bgtasks/tracer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/license/bgtasks/tracer.py b/apiserver/plane/license/bgtasks/tracer.py index f350e9a45b..26efb45ed2 100644 --- a/apiserver/plane/license/bgtasks/tracer.py +++ b/apiserver/plane/license/bgtasks/tracer.py @@ -82,6 +82,7 @@ def instance_traces(): # Set span attributes with tracer.start_as_current_span("workspace_details") as span: + span.set_attribute("instance_id", instance.instance_id) span.set_attribute("workspace_id", str(workspace.id)) span.set_attribute("workspace_slug", workspace.slug) span.set_attribute("project_count", project_count) From c92fe6191e7cbd6101f181d5c893195ea500f8bc Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:23:37 +0530 Subject: [PATCH 3/3] [WEB-2600] fix: estimate point deletion (#5762) * chore: only delete the cascade fields * chore: logged the issue activity --- apiserver/plane/app/views/estimate/base.py | 64 ++++++++++++++++++- apiserver/plane/app/views/project/base.py | 2 +- apiserver/plane/bgtasks/deletion_task.py | 34 ++++++---- .../plane/bgtasks/issue_activities_task.py | 10 +-- 4 files changed, 87 insertions(+), 23 deletions(-) diff --git a/apiserver/plane/app/views/estimate/base.py b/apiserver/plane/app/views/estimate/base.py index f384880019..80943c05eb 100644 --- a/apiserver/plane/app/views/estimate/base.py +++ b/apiserver/plane/app/views/estimate/base.py @@ -1,5 +1,9 @@ import random import string +import json + +# Django imports +from django.utils import timezone # Third party imports from rest_framework.response import Response @@ -19,6 +23,7 @@ from plane.app.serializers import ( EstimateReadSerializer, ) from plane.utils.cache import invalidate_cache +from plane.bgtasks.issue_activities_task import issue_activity def generate_random_name(length=10): @@ -249,11 +254,66 @@ class EstimatePointEndpoint(BaseViewSet): ) # update all the issues with the new estimate if new_estimate_id: - _ = Issue.objects.filter( + issues = Issue.objects.filter( project_id=project_id, workspace__slug=slug, estimate_point_id=estimate_point_id, - ).update(estimate_point_id=new_estimate_id) + ) + for issue in issues: + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps( + { + "estimate_point": ( + str(new_estimate_id) + if new_estimate_id + else None + ), + } + ), + actor_id=str(request.user.id), + issue_id=issue.id, + project_id=str(project_id), + current_instance=json.dumps( + { + "estimate_point": ( + str(issue.estimate_point_id) + if issue.estimate_point_id + else None + ), + } + ), + epoch=int(timezone.now().timestamp()), + ) + issues.update(estimate_point_id=new_estimate_id) + else: + issues = Issue.objects.filter( + project_id=project_id, + workspace__slug=slug, + estimate_point_id=estimate_point_id, + ) + for issue in issues: + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps( + { + "estimate_point": None, + } + ), + actor_id=str(request.user.id), + issue_id=issue.id, + project_id=str(project_id), + current_instance=json.dumps( + { + "estimate_point": ( + str(issue.estimate_point_id) + if issue.estimate_point_id + else None + ), + } + ), + epoch=int(timezone.now().timestamp()), + ) # delete the estimate point old_estimate_point = EstimatePoint.objects.filter( diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 8bdff03e62..6a9afb6523 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -413,7 +413,7 @@ class ProjectViewSet(BaseViewSet): status=status.HTTP_410_GONE, ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") + @allow_permission([ROLE.ADMIN]) def partial_update(self, request, slug, pk=None): try: workspace = Workspace.objects.get(slug=slug) diff --git a/apiserver/plane/bgtasks/deletion_task.py b/apiserver/plane/bgtasks/deletion_task.py index d8272591a9..35d72812da 100644 --- a/apiserver/plane/bgtasks/deletion_task.py +++ b/apiserver/plane/bgtasks/deletion_task.py @@ -2,6 +2,7 @@ from django.utils import timezone from django.apps import apps from django.conf import settings +from django.db import models from django.core.exceptions import ObjectDoesNotExist # Third party imports @@ -18,17 +19,25 @@ def soft_delete_related_objects( for field in related_fields: if field.one_to_many or field.one_to_one: try: - if field.one_to_many: - related_objects = getattr(instance, field.name).all() - elif field.one_to_one: - related_object = getattr(instance, field.name) - related_objects = ( - [related_object] if related_object is not None else [] - ) - for obj in related_objects: - if obj: - obj.deleted_at = timezone.now() - obj.save(using=using) + # Check if the field has CASCADE on delete + if ( + hasattr(field.remote_field, "on_delete") + and field.remote_field.on_delete == models.CASCADE + ): + if field.one_to_many: + related_objects = getattr(instance, field.name).all() + elif field.one_to_one: + related_object = getattr(instance, field.name) + related_objects = ( + [related_object] + if related_object is not None + else [] + ) + + for obj in related_objects: + if obj: + obj.deleted_at = timezone.now() + obj.save(using=using) except ObjectDoesNotExist: pass @@ -154,8 +163,7 @@ def hard_delete(): if hasattr(model, "deleted_at"): # Get all instances where 'deleted_at' is greater than 30 days ago _ = model.all_objects.filter( - deleted_at__lt=timezone.now() - - timezone.timedelta(days=days) + deleted_at__lt=timezone.now() - timezone.timedelta(days=days) ).delete() return diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index 8ecf7845db..0cee9baef3 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -465,7 +465,7 @@ def track_estimate_points( IssueActivity( issue_id=issue_id, actor_id=actor_id, - verb="updated", + verb="removed" if new_estimate is None else "updated", old_identifier=( current_instance.get("estimate_point") if current_instance.get("estimate_point") is not None @@ -1700,16 +1700,12 @@ def issue_activity( event=( "issue_comment" if activity.field == "comment" - else "inbox_issue" - if inbox - else "issue" + else "inbox_issue" if inbox else "issue" ), event_id=( activity.issue_comment_id if activity.field == "comment" - else inbox - if inbox - else activity.issue_id + else inbox if inbox else activity.issue_id ), verb=activity.verb, field=(