Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 50 additions & 14 deletions django_email_learning/jobs/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,72 @@
from django.utils.decorators import method_decorator
from django.http import JsonResponse

from django_email_learning.models import JobName
from django_email_learning.services.metrics_service import MetricsService


metric_service = MetricsService()


@method_decorator(check_api_key(), name="get")
class DeliverContentsJobView(View):
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
job = DeliverContentsJob()
job.run()
return JsonResponse({"status": "DeliverContentsJob triggered"}, status=202)
try:
job = DeliverContentsJob()
job.run()
return JsonResponse({"status": "DeliverContentsJob triggered"}, status=202)
except Exception as e:
metric_service.job_execution_failed(job_name=JobName.DELIVER_CONTENTS.value)
return JsonResponse(
{"status": "DeliverContentsJob failed", "error": str(e)}, status=500
)


@method_decorator(check_api_key(), name="get")
class CheckIMAPJobView(View):
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
job = CheckIMAPJob()
job.run()
return JsonResponse({"status": "CheckIMAPJob triggered"}, status=202)
try:
job = CheckIMAPJob()
job.run()
return JsonResponse({"status": "CheckIMAPJob triggered"}, status=202)
except Exception as e:
metric_service.job_execution_failed(job_name=JobName.CHECK_IMAP.value)
return JsonResponse(
{"status": "CheckIMAPJob failed", "error": str(e)}, status=500
)


@method_decorator(check_api_key(), name="get")
class SendQuizRemindersJobView(View):
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
job = SendRemindersJob()
job.run()
return JsonResponse({"status": "SendRemidersJob triggered"}, status=202)
try:
job = SendRemindersJob()
job.run()
return JsonResponse({"status": "SendRemidersJob triggered"}, status=202)
except Exception as e:
metric_service.job_execution_failed(job_name=JobName.SEND_REMINDERS.value)
return JsonResponse(
{"status": "SendRemidersJob failed", "error": str(e)}, status=500
)


@method_decorator(check_api_key(), name="get")
class DeactivateInactiveEnrollmentsJobView(View):
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
job = DeactivateInactiveEnrollmentsJob()
job.run()
return JsonResponse(
{"status": "DeactivateInactiveEnrollmentsJob triggered"}, status=202
)
try:
job = DeactivateInactiveEnrollmentsJob()
job.run()
return JsonResponse(
{"status": "DeactivateInactiveEnrollmentsJob triggered"}, status=202
)
except Exception as e:
metric_service.job_execution_failed(
job_name=JobName.DEACTIVATE_ENROLLMENTS.value
)
return JsonResponse(
{
"status": "DeactivateInactiveEnrollmentsJob failed",
"error": str(e),
},
status=500,
)
19 changes: 14 additions & 5 deletions django_email_learning/jobs/check_imap_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
JobName,
JobStatus,
)
from django_email_learning.jobs.job_metrics import track_job_execution
from django_email_learning.services.metrics_service import MetricsService
from django_email_learning.ports.imap_interface_protocol import ImapInterfaceProtocol
from django.utils.module_loading import import_string
Expand All @@ -15,7 +16,7 @@
import logging

logger = logging.getLogger(__name__)
metricc_service = MetricsService()
metric_service = MetricsService()


class CheckIMAPJob:
Expand Down Expand Up @@ -54,6 +55,13 @@ def run(self) -> None:
status=JobStatus.RUNNING.value,
started_at=timezone.now(),
)
self._run_job(job_execution)

@track_job_execution(
metric_service=metric_service,
job_name=JobName.CHECK_IMAP.value,
)
def _run_job(self, job_execution: JobExecution) -> None:
imap_connections = ImapConnection.objects.filter(course__enabled=True)

for imap_connection in imap_connections:
Expand All @@ -73,11 +81,12 @@ def run(self) -> None:
for folder in folders:
self._process_folder(account, folder, imap_connection)
finally:
job_execution.status = JobStatus.COMPLETED.value
job_execution.finished_at = timezone.now()
job_execution.save()
self._logout_account(account, imap_connection)

job_execution.status = JobStatus.COMPLETED.value
job_execution.finished_at = timezone.now()
job_execution.save()

def _connect_account(
self, imap_connection: ImapConnection
) -> imaplib.IMAP4_SSL | None:
Expand Down Expand Up @@ -145,7 +154,7 @@ def _process_email(
f"Error processing email with ID {email_id} for connection {imap_connection.id}: {str(e)}",
exc_info=True,
)
metricc_service.imap_command_handling_failed(
metric_service.imap_command_handling_failed(
imap_connection_id=imap_connection.id,
organization_id=imap_connection.organization.id,
)
Expand Down
12 changes: 10 additions & 2 deletions django_email_learning/jobs/deactivate_inactive_enrollments_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
JobStatus,
Quiz,
)
from django_email_learning.jobs.job_metrics import track_job_execution
from django_email_learning.services.metrics_service import MetricsService
from django_email_learning.services.email_sender_service import EmailSenderService
from django.core.mail import EmailMultiAlternatives
Expand All @@ -18,7 +19,7 @@
from django_email_learning.services.utils import mask_email

logger = logging.getLogger(__name__)
metricc_service = MetricsService()
metric_service = MetricsService()


class DeactivateInactiveEnrollmentsJob:
Expand All @@ -37,6 +38,13 @@ def run(self) -> None:
status=JobStatus.RUNNING.value,
started_at=timezone.now(),
)
self._run_job(job_execution)

@track_job_execution(
metric_service=metric_service,
job_name=JobName.DEACTIVATE_ENROLLMENTS.value,
)
def _run_job(self, job_execution: JobExecution) -> None:
deliveries = ContentDelivery.objects.filter(
valid_until__lt=timezone.now(),
enrollment__status=EnrollmentStatus.ACTIVE,
Expand Down Expand Up @@ -75,7 +83,7 @@ def run(self) -> None:
course_title,
delivery.course_content.course.organization.name,
)
metricc_service.user_enrollment_deactivated(
metric_service.user_enrollment_deactivated(
course_slug=delivery.course_content.course.slug,
organization_id=delivery.course_content.course.organization.id,
reason=DeactivationReason.INACTIVE.value,
Expand Down
8 changes: 8 additions & 0 deletions django_email_learning/jobs/deliver_contents_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
SendQuizCommand,
QuizNotFoundError,
)
from django_email_learning.jobs.job_metrics import track_job_execution
from django_email_learning.services.metrics_service import MetricsService
from django_email_learning.models import JobExecution, JobName, JobStatus
from django.utils.module_loading import import_string
Expand Down Expand Up @@ -39,6 +40,13 @@ def run(self) -> None:
status=JobStatus.RUNNING.value,
started_at=timezone.now(),
)
self._run_job(job_execution)

@track_job_execution(
metric_service=METRIC_SERVICE,
job_name=JobName.DELIVER_CONTENTS.value,
)
def _run_job(self, job_execution: JobExecution) -> None:
should_check_next = True
while should_check_next:
delivery_schedule = self.delivery_queue.next_task()
Expand Down
33 changes: 33 additions & 0 deletions django_email_learning/jobs/job_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar

from django.utils import timezone


P = ParamSpec("P")
R = TypeVar("R")


def track_job_execution(
*,
metric_service: object,
job_name: str,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start_time = timezone.now()
metric_service.job_execution_started(job_name=job_name) # type: ignore[attr-defined]
try:
return func(*args, **kwargs)
finally:
execution_time = int((timezone.now() - start_time).total_seconds())
metric_service.job_execution_finished( # type: ignore[attr-defined]
job_name=job_name,
execution_time=execution_time,
)

return wrapper

return decorator
8 changes: 8 additions & 0 deletions django_email_learning/jobs/send_reminders_job.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django_email_learning.ports.delivery_queue_protocol import DeliveryQueueProtocol
from django_email_learning.models import ContentDelivery, DeliverySchedule
from django_email_learning.jobs.job_metrics import track_job_execution

from django_email_learning.services.metrics_service import MetricsService
from django_email_learning.models import JobExecution, JobName, JobStatus
Expand Down Expand Up @@ -35,6 +36,13 @@ def run(self) -> None:
status=JobStatus.RUNNING.value,
started_at=timezone.now(),
)
self._run_job(job_execution)

@track_job_execution(
metric_service=METRIC_SERVICE,
job_name=JobName.SEND_REMINDERS.value,
)
def _run_job(self, job_execution: JobExecution) -> None:
should_check_next = True
while should_check_next:
delivery_schedule = self.reminder_queue.next_task()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from django.core.management.base import BaseCommand
from django_email_learning.jobs.check_imap_job import CheckIMAPJob
from django.core.management.base import CommandParser
from django_email_learning.services.metrics_service import MetricsService
from django_email_learning.models import JobName
import logging


metric_service = MetricsService()


class Command(BaseCommand):
help = "Check the IMAP connection for all courses and execute valid email commands"

Expand Down Expand Up @@ -45,6 +50,7 @@ def handle(self, *args, **options) -> None: # type: ignore[no-untyped-def]
logger.warning("Check IMAP job interrupted by user")

except Exception as e:
metric_service.job_execution_failed(job_name=JobName.CHECK_IMAP.value)
self.stdout.write(self.style.ERROR(f"Check IMAP job failed: {str(e)}"))
logger.error(f"Check IMAP job failed: {str(e)}", exc_info=True)
raise
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
from django.core.management.base import CommandParser
import logging

from django_email_learning.models import JobName
from django_email_learning.services.metrics_service import MetricsService


metric_service = MetricsService()


class Command(BaseCommand):
help = "Run the deactivate inactive enrollments job to deactivate enrollments that have missed quiz deadlines"
Expand Down Expand Up @@ -53,6 +59,9 @@ def handle(self, *args, **options) -> None: # type: ignore[no-untyped-def]
logger.warning("DeactivateInactiveEnrollmentsJob interrupted by user")

except Exception as e:
metric_service.job_execution_failed(
job_name=JobName.DEACTIVATE_ENROLLMENTS.value
)
self.stdout.write(
self.style.ERROR(
f"Deactivate inactive enrollments job failed: {str(e)}"
Expand Down
7 changes: 7 additions & 0 deletions django_email_learning/management/commands/deliver_contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
from django.core.management.base import CommandParser
import logging

from django_email_learning.models import JobName
from django_email_learning.services.metrics_service import MetricsService


metric_service = MetricsService()


class Command(BaseCommand):
help = "Run the content delivery job to process scheduled content deliveries"
Expand Down Expand Up @@ -47,6 +53,7 @@ def handle(self, *args, **options) -> None: # type: ignore[no-untyped-def]
logger.warning("DeliverContentsJob interrupted by user")

except Exception as e:
metric_service.job_execution_failed(job_name=JobName.DELIVER_CONTENTS.value)
self.stdout.write(
self.style.ERROR(f"Content delivery job failed: {str(e)}")
)
Expand Down
7 changes: 7 additions & 0 deletions django_email_learning/management/commands/send_reminders.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
from django.core.management.base import CommandParser
import logging

from django_email_learning.models import JobName
from django_email_learning.services.metrics_service import MetricsService


metric_service = MetricsService()


class Command(BaseCommand):
help = "Run the send reminders job to process scheduled reminders"
Expand Down Expand Up @@ -47,6 +53,7 @@ def handle(self, *args, **options) -> None: # type: ignore[no-untyped-def]
logger.warning("SendRemindersJob interrupted by user")

except Exception as e:
metric_service.job_execution_failed(job_name=JobName.SEND_REMINDERS.value)
self.stdout.write(self.style.ERROR(f"Send reminders job failed: {str(e)}"))
logger.error(f"SendRemindersJob failed: {str(e)}", exc_info=True)
raise
9 changes: 9 additions & 0 deletions django_email_learning/ports/metric_recorder_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,12 @@ def imap_command_handling_failed(
self, imap_connection_id: int, organization_id: int
) -> None:
...

def job_execution_started(self, job_name: str) -> None:
...

def job_execution_finished(self, job_name: str, execution_time: int) -> None:
...

def job_execution_failed(self, job_name: str) -> None:
...
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,31 @@ def imap_command_handling_failed(
"organization_id": organization_id,
},
)

def job_execution_started(self, job_name: str) -> None:
logger.info(
"Job execution started",
extra={
"metric": "job_execution_started",
"job_name": job_name,
},
)

def job_execution_finished(self, job_name: str, execution_time: int) -> None:
logger.info(
"Job execution finished",
extra={
"metric": "job_execution_finished",
"job_name": job_name,
"execution_time": execution_time,
},
)

def job_execution_failed(self, job_name: str) -> None:
logger.error(
"Job execution failed",
extra={
"metric": "job_execution_failed",
"job_name": job_name,
},
)
Loading