Skip to content

Commit 2ac7e53

Browse files
committed
feat: #383 add metrics for job started/completed/failed
1 parent 39c2c37 commit 2ac7e53

22 files changed

Lines changed: 541 additions & 28 deletions

django_email_learning/jobs/api/views.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,72 @@
99
from django.utils.decorators import method_decorator
1010
from django.http import JsonResponse
1111

12+
from django_email_learning.models import JobName
13+
from django_email_learning.services.metrics_service import MetricsService
14+
15+
16+
metric_service = MetricsService()
17+
1218

1319
@method_decorator(check_api_key(), name="get")
1420
class DeliverContentsJobView(View):
1521
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
16-
job = DeliverContentsJob()
17-
job.run()
18-
return JsonResponse({"status": "DeliverContentsJob triggered"}, status=202)
22+
try:
23+
job = DeliverContentsJob()
24+
job.run()
25+
return JsonResponse({"status": "DeliverContentsJob triggered"}, status=202)
26+
except Exception as e:
27+
metric_service.job_execution_failed(job_name=JobName.DELIVER_CONTENTS.value)
28+
return JsonResponse(
29+
{"status": "DeliverContentsJob failed", "error": str(e)}, status=500
30+
)
1931

2032

2133
@method_decorator(check_api_key(), name="get")
2234
class CheckIMAPJobView(View):
2335
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
24-
job = CheckIMAPJob()
25-
job.run()
26-
return JsonResponse({"status": "CheckIMAPJob triggered"}, status=202)
36+
try:
37+
job = CheckIMAPJob()
38+
job.run()
39+
return JsonResponse({"status": "CheckIMAPJob triggered"}, status=202)
40+
except Exception as e:
41+
metric_service.job_execution_failed(job_name=JobName.CHECK_IMAP.value)
42+
return JsonResponse(
43+
{"status": "CheckIMAPJob failed", "error": str(e)}, status=500
44+
)
2745

2846

2947
@method_decorator(check_api_key(), name="get")
3048
class SendQuizRemindersJobView(View):
3149
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
32-
job = SendRemindersJob()
33-
job.run()
34-
return JsonResponse({"status": "SendRemidersJob triggered"}, status=202)
50+
try:
51+
job = SendRemindersJob()
52+
job.run()
53+
return JsonResponse({"status": "SendRemidersJob triggered"}, status=202)
54+
except Exception as e:
55+
metric_service.job_execution_failed(job_name=JobName.SEND_REMINDERS.value)
56+
return JsonResponse(
57+
{"status": "SendRemidersJob failed", "error": str(e)}, status=500
58+
)
3559

3660

3761
@method_decorator(check_api_key(), name="get")
3862
class DeactivateInactiveEnrollmentsJobView(View):
3963
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
40-
job = DeactivateInactiveEnrollmentsJob()
41-
job.run()
42-
return JsonResponse(
43-
{"status": "DeactivateInactiveEnrollmentsJob triggered"}, status=202
44-
)
64+
try:
65+
job = DeactivateInactiveEnrollmentsJob()
66+
job.run()
67+
return JsonResponse(
68+
{"status": "DeactivateInactiveEnrollmentsJob triggered"}, status=202
69+
)
70+
except Exception as e:
71+
metric_service.job_execution_failed(
72+
job_name=JobName.DEACTIVATE_ENROLLMENTS.value
73+
)
74+
return JsonResponse(
75+
{
76+
"status": "DeactivateInactiveEnrollmentsJob failed",
77+
"error": str(e),
78+
},
79+
status=500,
80+
)

django_email_learning/jobs/check_imap_job.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
JobName,
55
JobStatus,
66
)
7+
from django_email_learning.jobs.job_metrics import track_job_execution
78
from django_email_learning.services.metrics_service import MetricsService
89
from django_email_learning.ports.imap_interface_protocol import ImapInterfaceProtocol
910
from django.utils.module_loading import import_string
@@ -15,7 +16,7 @@
1516
import logging
1617

1718
logger = logging.getLogger(__name__)
18-
metricc_service = MetricsService()
19+
metric_service = MetricsService()
1920

2021

2122
class CheckIMAPJob:
@@ -54,6 +55,13 @@ def run(self) -> None:
5455
status=JobStatus.RUNNING.value,
5556
started_at=timezone.now(),
5657
)
58+
self._run_job(job_execution)
59+
60+
@track_job_execution(
61+
metric_service=metric_service,
62+
job_name=JobName.CHECK_IMAP.value,
63+
)
64+
def _run_job(self, job_execution: JobExecution) -> None:
5765
imap_connections = ImapConnection.objects.filter(course__enabled=True)
5866

5967
for imap_connection in imap_connections:
@@ -73,11 +81,12 @@ def run(self) -> None:
7381
for folder in folders:
7482
self._process_folder(account, folder, imap_connection)
7583
finally:
76-
job_execution.status = JobStatus.COMPLETED.value
77-
job_execution.finished_at = timezone.now()
78-
job_execution.save()
7984
self._logout_account(account, imap_connection)
8085

86+
job_execution.status = JobStatus.COMPLETED.value
87+
job_execution.finished_at = timezone.now()
88+
job_execution.save()
89+
8190
def _connect_account(
8291
self, imap_connection: ImapConnection
8392
) -> imaplib.IMAP4_SSL | None:
@@ -145,7 +154,7 @@ def _process_email(
145154
f"Error processing email with ID {email_id} for connection {imap_connection.id}: {str(e)}",
146155
exc_info=True,
147156
)
148-
metricc_service.imap_command_handling_failed(
157+
metric_service.imap_command_handling_failed(
149158
imap_connection_id=imap_connection.id,
150159
organization_id=imap_connection.organization.id,
151160
)

django_email_learning/jobs/deactivate_inactive_enrollments_job.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
JobStatus,
88
Quiz,
99
)
10+
from django_email_learning.jobs.job_metrics import track_job_execution
1011
from django_email_learning.services.metrics_service import MetricsService
1112
from django_email_learning.services.email_sender_service import EmailSenderService
1213
from django.core.mail import EmailMultiAlternatives
@@ -18,7 +19,7 @@
1819
from django_email_learning.services.utils import mask_email
1920

2021
logger = logging.getLogger(__name__)
21-
metricc_service = MetricsService()
22+
metric_service = MetricsService()
2223

2324

2425
class DeactivateInactiveEnrollmentsJob:
@@ -37,6 +38,13 @@ def run(self) -> None:
3738
status=JobStatus.RUNNING.value,
3839
started_at=timezone.now(),
3940
)
41+
self._run_job(job_execution)
42+
43+
@track_job_execution(
44+
metric_service=metric_service,
45+
job_name=JobName.DEACTIVATE_ENROLLMENTS.value,
46+
)
47+
def _run_job(self, job_execution: JobExecution) -> None:
4048
deliveries = ContentDelivery.objects.filter(
4149
valid_until__lt=timezone.now(),
4250
enrollment__status=EnrollmentStatus.ACTIVE,
@@ -75,7 +83,7 @@ def run(self) -> None:
7583
course_title,
7684
delivery.course_content.course.organization.name,
7785
)
78-
metricc_service.user_enrollment_deactivated(
86+
metric_service.user_enrollment_deactivated(
7987
course_slug=delivery.course_content.course.slug,
8088
organization_id=delivery.course_content.course.organization.id,
8189
reason=DeactivationReason.INACTIVE.value,

django_email_learning/jobs/deliver_contents_job.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
SendQuizCommand,
99
QuizNotFoundError,
1010
)
11+
from django_email_learning.jobs.job_metrics import track_job_execution
1112
from django_email_learning.services.metrics_service import MetricsService
1213
from django_email_learning.models import JobExecution, JobName, JobStatus
1314
from django.utils.module_loading import import_string
@@ -39,6 +40,13 @@ def run(self) -> None:
3940
status=JobStatus.RUNNING.value,
4041
started_at=timezone.now(),
4142
)
43+
self._run_job(job_execution)
44+
45+
@track_job_execution(
46+
metric_service=METRIC_SERVICE,
47+
job_name=JobName.DELIVER_CONTENTS.value,
48+
)
49+
def _run_job(self, job_execution: JobExecution) -> None:
4250
should_check_next = True
4351
while should_check_next:
4452
delivery_schedule = self.delivery_queue.next_task()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from collections.abc import Callable
2+
from functools import wraps
3+
from typing import ParamSpec, TypeVar
4+
5+
from django.utils import timezone
6+
7+
8+
P = ParamSpec("P")
9+
R = TypeVar("R")
10+
11+
12+
def track_job_execution(
13+
*,
14+
metric_service: object,
15+
job_name: str,
16+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
17+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
18+
@wraps(func)
19+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
20+
start_time = timezone.now()
21+
metric_service.job_execution_started(job_name=job_name) # type: ignore[attr-defined]
22+
try:
23+
return func(*args, **kwargs)
24+
finally:
25+
execution_time = int((timezone.now() - start_time).total_seconds())
26+
metric_service.job_execution_finished( # type: ignore[attr-defined]
27+
job_name=job_name,
28+
execution_time=execution_time,
29+
)
30+
31+
return wrapper
32+
33+
return decorator

django_email_learning/jobs/send_reminders_job.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django_email_learning.ports.delivery_queue_protocol import DeliveryQueueProtocol
22
from django_email_learning.models import ContentDelivery, DeliverySchedule
3+
from django_email_learning.jobs.job_metrics import track_job_execution
34

45
from django_email_learning.services.metrics_service import MetricsService
56
from django_email_learning.models import JobExecution, JobName, JobStatus
@@ -35,6 +36,13 @@ def run(self) -> None:
3536
status=JobStatus.RUNNING.value,
3637
started_at=timezone.now(),
3738
)
39+
self._run_job(job_execution)
40+
41+
@track_job_execution(
42+
metric_service=METRIC_SERVICE,
43+
job_name=JobName.SEND_REMINDERS.value,
44+
)
45+
def _run_job(self, job_execution: JobExecution) -> None:
3846
should_check_next = True
3947
while should_check_next:
4048
delivery_schedule = self.reminder_queue.next_task()

django_email_learning/management/commands/check_imap_connections.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
from django.core.management.base import BaseCommand
22
from django_email_learning.jobs.check_imap_job import CheckIMAPJob
33
from django.core.management.base import CommandParser
4+
from django_email_learning.services.metrics_service import MetricsService
5+
from django_email_learning.models import JobName
46
import logging
57

68

9+
metric_service = MetricsService()
10+
11+
712
class Command(BaseCommand):
813
help = "Check the IMAP connection for all courses and execute valid email commands"
914

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

4752
except Exception as e:
53+
metric_service.job_execution_failed(job_name=JobName.CHECK_IMAP.value)
4854
self.stdout.write(self.style.ERROR(f"Check IMAP job failed: {str(e)}"))
4955
logger.error(f"Check IMAP job failed: {str(e)}", exc_info=True)
5056
raise

django_email_learning/management/commands/deactivate_inactive_enrollments.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
from django.core.management.base import CommandParser
66
import logging
77

8+
from django_email_learning.models import JobName
9+
from django_email_learning.services.metrics_service import MetricsService
10+
11+
12+
metric_service = MetricsService()
13+
814

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

5561
except Exception as e:
62+
metric_service.job_execution_failed(
63+
job_name=JobName.DEACTIVATE_ENROLLMENTS.value
64+
)
5665
self.stdout.write(
5766
self.style.ERROR(
5867
f"Deactivate inactive enrollments job failed: {str(e)}"

django_email_learning/management/commands/deliver_contents.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
from django.core.management.base import CommandParser
44
import logging
55

6+
from django_email_learning.models import JobName
7+
from django_email_learning.services.metrics_service import MetricsService
8+
9+
10+
metric_service = MetricsService()
11+
612

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

4955
except Exception as e:
56+
metric_service.job_execution_failed(job_name=JobName.DELIVER_CONTENTS.value)
5057
self.stdout.write(
5158
self.style.ERROR(f"Content delivery job failed: {str(e)}")
5259
)

django_email_learning/management/commands/send_reminders.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
from django.core.management.base import CommandParser
44
import logging
55

6+
from django_email_learning.models import JobName
7+
from django_email_learning.services.metrics_service import MetricsService
8+
9+
10+
metric_service = MetricsService()
11+
612

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

4955
except Exception as e:
56+
metric_service.job_execution_failed(job_name=JobName.SEND_REMINDERS.value)
5057
self.stdout.write(self.style.ERROR(f"Send reminders job failed: {str(e)}"))
5158
logger.error(f"SendRemindersJob failed: {str(e)}", exc_info=True)
5259
raise

0 commit comments

Comments
 (0)