django-nativemojo 0.1.15__py3-none-any.whl → 0.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -0,0 +1,317 @@
|
|
1
|
+
"""
|
2
|
+
Webhook job handler for Django-MOJO Jobs System.
|
3
|
+
|
4
|
+
Specialized handler for sending webhooks (HTTP POST requests) with proper
|
5
|
+
retry logic, timeout handling, and comprehensive logging.
|
6
|
+
"""
|
7
|
+
import json
|
8
|
+
import requests
|
9
|
+
from datetime import datetime, timezone
|
10
|
+
from typing import Dict, Any, Optional
|
11
|
+
from urllib.parse import urlparse
|
12
|
+
|
13
|
+
from django.conf import settings
|
14
|
+
|
15
|
+
from mojo.helpers import logit
|
16
|
+
from mojo.apps.jobs.models import Job
|
17
|
+
|
18
|
+
|
19
|
+
def post_webhook(job: Job) -> str:
|
20
|
+
"""
|
21
|
+
Send a webhook POST request with comprehensive error handling and logging.
|
22
|
+
|
23
|
+
Expected payload:
|
24
|
+
url: Target webhook URL (required)
|
25
|
+
data: Data to POST as JSON (required)
|
26
|
+
headers: HTTP headers dict (optional)
|
27
|
+
timeout: Request timeout in seconds (default: 30)
|
28
|
+
webhook_id: Optional webhook identifier for tracking
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
str: 'success', 'failed', or 'cancelled'
|
32
|
+
|
33
|
+
The job will be automatically retried based on the job's retry configuration
|
34
|
+
when certain errors occur (network errors, 5xx responses, timeouts).
|
35
|
+
"""
|
36
|
+
# Extract payload
|
37
|
+
url = job.payload.get('url')
|
38
|
+
data = job.payload.get('data')
|
39
|
+
headers = job.payload.get('headers', {})
|
40
|
+
timeout = job.payload.get('timeout', getattr(settings, 'JOBS_WEBHOOK_DEFAULT_TIMEOUT', 30))
|
41
|
+
webhook_id = job.payload.get('webhook_id')
|
42
|
+
|
43
|
+
# Validate required fields
|
44
|
+
if not url:
|
45
|
+
job.metadata['error'] = 'Missing required field: url'
|
46
|
+
job.metadata['failed_at'] = datetime.now(timezone.utc).isoformat()
|
47
|
+
return 'failed'
|
48
|
+
|
49
|
+
if data is None:
|
50
|
+
job.metadata['error'] = 'Missing required field: data'
|
51
|
+
job.metadata['failed_at'] = datetime.now(timezone.utc).isoformat()
|
52
|
+
return 'failed'
|
53
|
+
|
54
|
+
# Check for cancellation
|
55
|
+
if job.cancel_requested:
|
56
|
+
job.metadata['cancelled'] = True
|
57
|
+
job.metadata['cancelled_at'] = datetime.now(timezone.utc).isoformat()
|
58
|
+
logit.info(f"Webhook job {job.id} cancelled before execution")
|
59
|
+
return 'cancelled'
|
60
|
+
|
61
|
+
# Initialize tracking metadata
|
62
|
+
start_time = datetime.now(timezone.utc)
|
63
|
+
job.metadata.update({
|
64
|
+
'webhook_started_at': start_time.isoformat(),
|
65
|
+
'url': url,
|
66
|
+
'webhook_id': webhook_id,
|
67
|
+
'attempt': job.attempt,
|
68
|
+
'timeout_seconds': timeout,
|
69
|
+
'headers_sent': _sanitize_headers(headers)
|
70
|
+
})
|
71
|
+
|
72
|
+
try:
|
73
|
+
# Parse URL for validation and logging
|
74
|
+
parsed_url = urlparse(url)
|
75
|
+
job.metadata['parsed_url'] = {
|
76
|
+
'scheme': parsed_url.scheme,
|
77
|
+
'netloc': parsed_url.netloc,
|
78
|
+
'path': parsed_url.path
|
79
|
+
}
|
80
|
+
|
81
|
+
logit.info(f"Sending webhook {job.id} to {parsed_url.netloc}{parsed_url.path} "
|
82
|
+
f"(attempt {job.attempt})")
|
83
|
+
|
84
|
+
# Make the request
|
85
|
+
response = requests.post(
|
86
|
+
url=url,
|
87
|
+
json=data, # Auto JSON encoding and Content-Type header
|
88
|
+
headers=headers,
|
89
|
+
timeout=timeout,
|
90
|
+
allow_redirects=True # Follow redirects for webhooks
|
91
|
+
)
|
92
|
+
|
93
|
+
# Calculate duration
|
94
|
+
end_time = datetime.now(timezone.utc)
|
95
|
+
duration_ms = int((end_time - start_time).total_seconds() * 1000)
|
96
|
+
|
97
|
+
# Store response metadata
|
98
|
+
job.metadata.update({
|
99
|
+
'response_status_code': response.status_code,
|
100
|
+
'response_headers': dict(response.headers),
|
101
|
+
'response_size_bytes': len(response.content),
|
102
|
+
'duration_ms': duration_ms,
|
103
|
+
'webhook_completed_at': end_time.isoformat()
|
104
|
+
})
|
105
|
+
|
106
|
+
# Store response body sample (first 1KB for debugging)
|
107
|
+
if response.content:
|
108
|
+
try:
|
109
|
+
# Try to parse as JSON first
|
110
|
+
response_json = response.json()
|
111
|
+
if isinstance(response_json, dict):
|
112
|
+
# Store first 10 keys for debugging
|
113
|
+
sample = {k: v for k, v in list(response_json.items())[:10]}
|
114
|
+
job.metadata['response_sample'] = sample
|
115
|
+
elif isinstance(response_json, list):
|
116
|
+
job.metadata['response_count'] = len(response_json)
|
117
|
+
if response_json:
|
118
|
+
job.metadata['response_sample'] = response_json[0]
|
119
|
+
except:
|
120
|
+
# Not JSON, store text sample
|
121
|
+
text_sample = response.text[:1000]
|
122
|
+
job.metadata['response_text_sample'] = text_sample
|
123
|
+
|
124
|
+
# Check response status
|
125
|
+
try:
|
126
|
+
response.raise_for_status()
|
127
|
+
|
128
|
+
# Success!
|
129
|
+
logit.info(f"Webhook {job.id} delivered successfully to {parsed_url.netloc} "
|
130
|
+
f"(status {response.status_code}, {duration_ms}ms)")
|
131
|
+
|
132
|
+
# Emit success metrics
|
133
|
+
_emit_webhook_metrics('success', duration_ms, parsed_url.netloc)
|
134
|
+
|
135
|
+
return 'success'
|
136
|
+
|
137
|
+
except requests.exceptions.HTTPError as e:
|
138
|
+
# HTTP error response
|
139
|
+
status_code = response.status_code
|
140
|
+
job.metadata.update({
|
141
|
+
'error_type': 'http_error',
|
142
|
+
'error_status_code': status_code,
|
143
|
+
'error_message': f"HTTP {status_code}: {response.reason}"
|
144
|
+
})
|
145
|
+
|
146
|
+
# Determine if we should retry based on status code
|
147
|
+
retriable_codes = [408, 429, 502, 503, 504, 520, 521, 522, 523, 524]
|
148
|
+
|
149
|
+
if status_code in retriable_codes:
|
150
|
+
# Server/network error - retry
|
151
|
+
logit.warn(f"Webhook {job.id} received retriable HTTP {status_code}, will retry")
|
152
|
+
_emit_webhook_metrics('error_retriable', duration_ms, parsed_url.netloc)
|
153
|
+
raise # This will trigger retry logic
|
154
|
+
|
155
|
+
elif 400 <= status_code < 500:
|
156
|
+
# Client error - don't retry
|
157
|
+
logit.error(f"Webhook {job.id} failed with client error HTTP {status_code}")
|
158
|
+
_emit_webhook_metrics('error_client', duration_ms, parsed_url.netloc)
|
159
|
+
return 'failed'
|
160
|
+
|
161
|
+
else:
|
162
|
+
# Other error - retry
|
163
|
+
logit.warn(f"Webhook {job.id} received HTTP {status_code}, will retry")
|
164
|
+
_emit_webhook_metrics('error_retriable', duration_ms, parsed_url.netloc)
|
165
|
+
raise
|
166
|
+
|
167
|
+
except requests.exceptions.Timeout:
|
168
|
+
job.metadata.update({
|
169
|
+
'error_type': 'timeout',
|
170
|
+
'error_message': f'Request timed out after {timeout} seconds'
|
171
|
+
})
|
172
|
+
logit.warn(f"Webhook {job.id} timed out after {timeout}s, will retry")
|
173
|
+
_emit_webhook_metrics('timeout', None, parsed_url.netloc if 'parsed_url' in locals() else 'unknown')
|
174
|
+
raise # Retry on timeout
|
175
|
+
|
176
|
+
except requests.exceptions.ConnectionError as e:
|
177
|
+
job.metadata.update({
|
178
|
+
'error_type': 'connection_error',
|
179
|
+
'error_message': f'Connection failed: {str(e)}'
|
180
|
+
})
|
181
|
+
logit.warn(f"Webhook {job.id} connection failed, will retry: {e}")
|
182
|
+
_emit_webhook_metrics('connection_error', None, parsed_url.netloc if 'parsed_url' in locals() else 'unknown')
|
183
|
+
raise # Retry on connection errors
|
184
|
+
|
185
|
+
except requests.exceptions.RequestException as e:
|
186
|
+
# Other requests errors - retry
|
187
|
+
job.metadata.update({
|
188
|
+
'error_type': 'request_error',
|
189
|
+
'error_message': str(e)
|
190
|
+
})
|
191
|
+
logit.warn(f"Webhook {job.id} request failed, will retry: {e}")
|
192
|
+
_emit_webhook_metrics('request_error', None, parsed_url.netloc if 'parsed_url' in locals() else 'unknown')
|
193
|
+
raise
|
194
|
+
|
195
|
+
except Exception as e:
|
196
|
+
# Unexpected error - don't retry
|
197
|
+
job.metadata.update({
|
198
|
+
'error_type': 'unexpected_error',
|
199
|
+
'error_message': str(e),
|
200
|
+
'failed_at': datetime.now(timezone.utc).isoformat()
|
201
|
+
})
|
202
|
+
logit.error(f"Webhook {job.id} failed with unexpected error: {e}")
|
203
|
+
_emit_webhook_metrics('unexpected_error', None, parsed_url.netloc if 'parsed_url' in locals() else 'unknown')
|
204
|
+
return 'failed'
|
205
|
+
|
206
|
+
|
207
|
+
def _sanitize_headers(headers: Dict[str, str]) -> Dict[str, str]:
|
208
|
+
"""
|
209
|
+
Sanitize headers for logging by masking sensitive values.
|
210
|
+
|
211
|
+
Args:
|
212
|
+
headers: Original headers dict
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
dict: Headers with sensitive values masked
|
216
|
+
"""
|
217
|
+
if not headers:
|
218
|
+
return {}
|
219
|
+
|
220
|
+
# Headers to mask
|
221
|
+
sensitive_headers = {
|
222
|
+
'authorization', 'x-api-key', 'x-auth-token', 'cookie',
|
223
|
+
'x-webhook-secret', 'x-hub-signature', 'x-signature'
|
224
|
+
}
|
225
|
+
|
226
|
+
sanitized = {}
|
227
|
+
for key, value in headers.items():
|
228
|
+
if key.lower() in sensitive_headers:
|
229
|
+
# Show just the first few characters
|
230
|
+
if isinstance(value, str) and len(value) > 8:
|
231
|
+
sanitized[key] = f"{value[:4]}...{value[-4:]}"
|
232
|
+
else:
|
233
|
+
sanitized[key] = "***masked***"
|
234
|
+
else:
|
235
|
+
sanitized[key] = value
|
236
|
+
|
237
|
+
return sanitized
|
238
|
+
|
239
|
+
|
240
|
+
def _emit_webhook_metrics(outcome: str, duration_ms: Optional[int], host: str):
|
241
|
+
"""
|
242
|
+
Emit webhook metrics for monitoring.
|
243
|
+
|
244
|
+
Args:
|
245
|
+
outcome: Outcome type (success, timeout, error_*, etc.)
|
246
|
+
duration_ms: Request duration in milliseconds (if available)
|
247
|
+
host: Target hostname
|
248
|
+
"""
|
249
|
+
try:
|
250
|
+
from mojo.apps import metrics
|
251
|
+
|
252
|
+
now = datetime.now(timezone.utc)
|
253
|
+
|
254
|
+
# Emit outcome metric
|
255
|
+
metrics.record(
|
256
|
+
slug=f"webhooks.{outcome}",
|
257
|
+
when=now,
|
258
|
+
count=1,
|
259
|
+
category="webhooks"
|
260
|
+
)
|
261
|
+
|
262
|
+
# Emit per-host metric
|
263
|
+
safe_host = host.replace('.', '_').replace('-', '_')[:50] # Safe metric name
|
264
|
+
metrics.record(
|
265
|
+
slug=f"webhooks.host.{safe_host}.{outcome}",
|
266
|
+
when=now,
|
267
|
+
count=1,
|
268
|
+
category="webhooks"
|
269
|
+
)
|
270
|
+
|
271
|
+
# Emit duration metric if available
|
272
|
+
if duration_ms is not None:
|
273
|
+
metrics.record(
|
274
|
+
slug="webhooks.duration_ms",
|
275
|
+
when=now,
|
276
|
+
count=duration_ms,
|
277
|
+
category="webhooks"
|
278
|
+
)
|
279
|
+
|
280
|
+
except Exception as e:
|
281
|
+
# Don't fail the job if metrics fail
|
282
|
+
logit.debug(f"Failed to emit webhook metrics: {e}")
|
283
|
+
|
284
|
+
|
285
|
+
def validate_webhook_payload(payload: Dict[str, Any]) -> Optional[str]:
|
286
|
+
"""
|
287
|
+
Validate webhook job payload.
|
288
|
+
|
289
|
+
Args:
|
290
|
+
payload: Job payload to validate
|
291
|
+
|
292
|
+
Returns:
|
293
|
+
str: Error message if invalid, None if valid
|
294
|
+
"""
|
295
|
+
if not isinstance(payload, dict):
|
296
|
+
return "Payload must be a dictionary"
|
297
|
+
|
298
|
+
if 'url' not in payload:
|
299
|
+
return "Missing required field: url"
|
300
|
+
|
301
|
+
if 'data' not in payload:
|
302
|
+
return "Missing required field: data"
|
303
|
+
|
304
|
+
url = payload['url']
|
305
|
+
if not isinstance(url, str) or not url.strip():
|
306
|
+
return "URL must be a non-empty string"
|
307
|
+
|
308
|
+
if not url.startswith(('http://', 'https://')):
|
309
|
+
return "URL must start with http:// or https://"
|
310
|
+
|
311
|
+
# Validate data is JSON serializable
|
312
|
+
try:
|
313
|
+
json.dumps(payload['data'])
|
314
|
+
except (TypeError, ValueError):
|
315
|
+
return "Data field must be JSON serializable"
|
316
|
+
|
317
|
+
return None
|