From d80a5744478f26ed414dd8cb5e208ca2cd7692dd Mon Sep 17 00:00:00 2001 From: Roman Donchenko Date: Wed, 4 Sep 2024 15:37:32 +0300 Subject: [PATCH] Log events for function calls (#8395) This will let us collect statistics about function usage. Note that I would've preferred the function ID to go into the `obj_id` field, but function IDs are strings, and the field is numeric. --- .../20240903_155336_roman_function_events.md | 4 +++ cvat/apps/events/event.py | 1 + cvat/apps/events/handlers.py | 20 +++++++++++++ cvat/apps/lambda_manager/views.py | 29 +++++++++++++++---- .../docs/administration/advanced/analytics.md | 2 ++ 5 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 changelog.d/20240903_155336_roman_function_events.md diff --git a/changelog.d/20240903_155336_roman_function_events.md b/changelog.d/20240903_155336_roman_function_events.md new file mode 100644 index 00000000000..1673e633634 --- /dev/null +++ b/changelog.d/20240903_155336_roman_function_events.md @@ -0,0 +1,4 @@ +### Added + +- Added analytics events for function calls + () diff --git a/cvat/apps/events/event.py b/cvat/apps/events/event.py index eefb3cdbaa0..ae519b56864 100644 --- a/cvat/apps/events/event.py +++ b/cvat/apps/events/event.py @@ -27,6 +27,7 @@ class EventScopes: "annotations": ["create", "update", "delete"], "label": ["create", "update", "delete"], "dataset": ["export", "import"], + "function": ["call"], } @classmethod diff --git a/cvat/apps/events/handlers.py b/cvat/apps/events/handlers.py index 4e92013d5f6..f2d3f757761 100644 --- a/cvat/apps/events/handlers.py +++ b/cvat/apps/events/handlers.py @@ -510,6 +510,26 @@ def handle_dataset_import( ) -> None: handle_dataset_io(instance, "import", format_name=format_name, cloud_storage_id=cloud_storage_id) +def handle_function_call( + function_id: str, + target: Union[Task, Job], + **payload_fields, +) -> None: + record_server_event( + scope=event_scope("call", "function"), + request_id=request_id(), + project_id=project_id(target), + task_id=task_id(target), + job_id=job_id(target), + user_id=user_id(), + user_name=user_name(), + user_email=user_email(), + payload={ + "function": {"id": function_id}, + **payload_fields, + }, + ) + def handle_rq_exception(rq_job, exc_type, exc_value, tb): oid = rq_job.meta.get(RQJobMetaField.ORG_ID, None) oslug = rq_job.meta.get(RQJobMetaField.ORG_SLUG, None) diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index 306ba6f8c86..e22441e0537 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -20,6 +20,7 @@ import numpy as np import requests import rq +from cvat.apps.events.handlers import handle_function_call from cvat.apps.lambda_manager.signals import interactive_function_call_signal from django.conf import settings from django.core.exceptions import ObjectDoesNotExist, ValidationError @@ -131,6 +132,12 @@ def _invoke_directly(self, func, payload): return response class LambdaFunction: + FRAME_PARAMETERS = ( + ('frame', 'frame'), + ('frame0', 'start frame'), + ('frame1', 'end frame'), + ) + def __init__(self, gateway, data): # ID of the function (e.g. omz.public.yolo-v3) self.id = data['metadata']['name'] @@ -372,11 +379,7 @@ def validate_attributes_mapping(attributes_mapping, model_attributes, db_attribu data_start_frame = task_data.start_frame step = task_data.get_frame_step() - for key, desc in ( - ('frame', 'frame'), - ('frame0', 'start frame'), - ('frame1', 'end frame'), - ): + for key, desc in self.FRAME_PARAMETERS: if key not in data: continue @@ -1083,7 +1086,7 @@ def call(self, request, func_id): gateway = LambdaGateway() lambda_func = gateway.get(func_id) - return lambda_func.invoke( + response = lambda_func.invoke( db_task, request.data, # TODO: better to add validation via serializer for these data db_job=job, @@ -1091,6 +1094,18 @@ def call(self, request, func_id): request=request ) + handle_function_call(func_id, db_task, + category="interactive", + parameters={ + param_name: param_value + for param_name, _ in LambdaFunction.FRAME_PARAMETERS + for param_value in [request.data.get(param_name)] + if param_value is not None + }, + ) + + return response + @extend_schema(tags=['lambda']) @extend_schema_view( retrieve=extend_schema( @@ -1182,6 +1197,8 @@ def create(self, request): rq_job = queue.enqueue(lambda_func, threshold, task, quality, mapping, cleanup, conv_mask_to_poly, max_distance, request, job=job) + handle_function_call(function, job or task, category="batch") + response_serializer = FunctionCallSerializer(rq_job.to_dict()) return response_serializer.data diff --git a/site/content/en/docs/administration/advanced/analytics.md b/site/content/en/docs/administration/advanced/analytics.md index c69d18db2b5..36425c341a3 100644 --- a/site/content/en/docs/administration/advanced/analytics.md +++ b/site/content/en/docs/administration/advanced/analytics.md @@ -129,6 +129,8 @@ Server events: - `export:dataset`, `import:dataset` +- `call:function` + Client events: - `load:cvat`