oxutils 0.1.0__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.
- oxutils/__init__.py +23 -0
- oxutils/apps.py +14 -0
- oxutils/audit/__init__.py +0 -0
- oxutils/audit/apps.py +12 -0
- oxutils/audit/export.py +229 -0
- oxutils/audit/masks.py +97 -0
- oxutils/audit/models.py +75 -0
- oxutils/audit/settings.py +19 -0
- oxutils/celery/__init__.py +1 -0
- oxutils/celery/base.py +98 -0
- oxutils/celery/settings.py +1 -0
- oxutils/conf.py +12 -0
- oxutils/enums/__init__.py +1 -0
- oxutils/enums/audit.py +8 -0
- oxutils/enums/invoices.py +11 -0
- oxutils/exceptions.py +117 -0
- oxutils/functions.py +99 -0
- oxutils/jwt/__init__.py +0 -0
- oxutils/jwt/auth.py +55 -0
- oxutils/jwt/client.py +123 -0
- oxutils/jwt/constants.py +1 -0
- oxutils/locale/fr/LC_MESSAGES/django.mo +0 -0
- oxutils/locale/fr/LC_MESSAGES/django.po +368 -0
- oxutils/logger/__init__.py +0 -0
- oxutils/logger/receivers.py +18 -0
- oxutils/logger/settings.py +63 -0
- oxutils/mixins/__init__.py +0 -0
- oxutils/mixins/base.py +21 -0
- oxutils/mixins/schemas.py +13 -0
- oxutils/mixins/services.py +146 -0
- oxutils/models/__init__.py +3 -0
- oxutils/models/base.py +116 -0
- oxutils/models/billing.py +140 -0
- oxutils/models/invoice.py +467 -0
- oxutils/py.typed +0 -0
- oxutils/s3/__init__.py +0 -0
- oxutils/s3/settings.py +34 -0
- oxutils/s3/storages.py +130 -0
- oxutils/settings.py +254 -0
- oxutils/types.py +13 -0
- oxutils-0.1.0.dist-info/METADATA +201 -0
- oxutils-0.1.0.dist-info/RECORD +43 -0
- oxutils-0.1.0.dist-info/WHEEL +4 -0
oxutils/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""OxUtils - Production-ready utilities for Django applications.
|
|
2
|
+
|
|
3
|
+
This package provides:
|
|
4
|
+
- JWT authentication with JWKS support
|
|
5
|
+
- S3 storage backends (static, media, private, logs)
|
|
6
|
+
- Structured logging with correlation IDs
|
|
7
|
+
- Audit system with S3 export
|
|
8
|
+
- Celery integration
|
|
9
|
+
- Django model mixins
|
|
10
|
+
- Custom exceptions
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
from oxutils.settings import oxi_settings
|
|
16
|
+
from oxutils.conf import UTILS_APPS, AUDIT_MIDDLEWARE
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"oxi_settings",
|
|
20
|
+
"UTILS_APPS",
|
|
21
|
+
"AUDIT_MIDDLEWARE",
|
|
22
|
+
"__version__",
|
|
23
|
+
]
|
oxutils/apps.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OxutilsConfig(AppConfig):
|
|
7
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
8
|
+
name = 'oxutils'
|
|
9
|
+
verbose_name = _("Oxiliere Utilities")
|
|
10
|
+
|
|
11
|
+
def ready(self):
|
|
12
|
+
import oxutils.logger.receivers
|
|
13
|
+
|
|
14
|
+
return super().ready()
|
|
File without changes
|
oxutils/audit/apps.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OxutilsConfig(AppConfig):
|
|
7
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
8
|
+
name = 'oxutils_export'
|
|
9
|
+
verbose_name = _("Oxutils Export")
|
|
10
|
+
|
|
11
|
+
def ready(self):
|
|
12
|
+
return super().ready()
|
oxutils/audit/export.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Export utilities for audit logs.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to export LogEntry records from auditlog,
|
|
5
|
+
compress them into ZIP files, and save them to LogExportState.
|
|
6
|
+
"""
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import zipfile
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from django.apps import apps
|
|
14
|
+
from django.core.files.base import ContentFile
|
|
15
|
+
from django.db.models import QuerySet
|
|
16
|
+
from django.utils import timezone
|
|
17
|
+
|
|
18
|
+
from oxutils.audit.models import LogExportState
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_logentry_model():
|
|
22
|
+
"""Get the LogEntry model from auditlog."""
|
|
23
|
+
return apps.get_model('auditlog', 'LogEntry')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def export_logs_from_date(
|
|
27
|
+
from_date: datetime,
|
|
28
|
+
to_date: Optional[datetime] = None,
|
|
29
|
+
batch_size: int = 5000
|
|
30
|
+
) -> LogExportState:
|
|
31
|
+
"""
|
|
32
|
+
Export audit logs from a specific date, compress them, and save to LogExportState.
|
|
33
|
+
Optimized for S3 storage with streaming and minimal memory usage.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
from_date: Start date for log export (inclusive)
|
|
37
|
+
to_date: End date for log export (inclusive). If None, uses current time.
|
|
38
|
+
batch_size: Number of records to process at a time (default: 5000)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
LogExportState: The created export state with the compressed data
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
Exception: If export fails, the LogExportState status will be set to FAILED
|
|
45
|
+
"""
|
|
46
|
+
if to_date is None:
|
|
47
|
+
to_date = timezone.now()
|
|
48
|
+
|
|
49
|
+
# Create the export state
|
|
50
|
+
export_state = LogExportState.create(size=0)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Get LogEntry model
|
|
54
|
+
LogEntry = get_logentry_model()
|
|
55
|
+
|
|
56
|
+
# Query logs within date range - use select_related for optimization
|
|
57
|
+
logs_queryset = LogEntry.objects.filter(
|
|
58
|
+
timestamp__gte=from_date,
|
|
59
|
+
timestamp__lte=to_date
|
|
60
|
+
).select_related('content_type', 'actor').order_by('timestamp')
|
|
61
|
+
|
|
62
|
+
# Create ZIP file in memory with optimal compression
|
|
63
|
+
zip_buffer = io.BytesIO()
|
|
64
|
+
|
|
65
|
+
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as zip_file:
|
|
66
|
+
# Export logs in batches using iterator to avoid loading all in memory
|
|
67
|
+
total_exported = 0
|
|
68
|
+
batch_number = 1
|
|
69
|
+
batch_logs = []
|
|
70
|
+
|
|
71
|
+
# Use iterator() to stream results from database
|
|
72
|
+
for log in logs_queryset.iterator(chunk_size=batch_size):
|
|
73
|
+
batch_logs.append(_serialize_log_entry(log))
|
|
74
|
+
|
|
75
|
+
# Write batch when it reaches batch_size
|
|
76
|
+
if len(batch_logs) >= batch_size:
|
|
77
|
+
filename = f'logs_batch_{batch_number:04d}.json'
|
|
78
|
+
# Use separators to minimize JSON size
|
|
79
|
+
zip_file.writestr(
|
|
80
|
+
filename,
|
|
81
|
+
json.dumps(batch_logs, separators=(',', ':'))
|
|
82
|
+
)
|
|
83
|
+
total_exported += len(batch_logs)
|
|
84
|
+
batch_number += 1
|
|
85
|
+
batch_logs = []
|
|
86
|
+
|
|
87
|
+
# Write remaining logs
|
|
88
|
+
if batch_logs:
|
|
89
|
+
filename = f'logs_batch_{batch_number:04d}.json'
|
|
90
|
+
zip_file.writestr(
|
|
91
|
+
filename,
|
|
92
|
+
json.dumps(batch_logs, separators=(',', ':'))
|
|
93
|
+
)
|
|
94
|
+
total_exported += len(batch_logs)
|
|
95
|
+
|
|
96
|
+
# Export metadata at the end (we now have accurate count)
|
|
97
|
+
metadata = {
|
|
98
|
+
'export_date': timezone.now().isoformat(),
|
|
99
|
+
'from_date': from_date.isoformat(),
|
|
100
|
+
'to_date': to_date.isoformat(),
|
|
101
|
+
'total_records': total_exported,
|
|
102
|
+
'batch_size': batch_size,
|
|
103
|
+
'total_batches': batch_number
|
|
104
|
+
}
|
|
105
|
+
zip_file.writestr('metadata.json', json.dumps(metadata, separators=(',', ':')))
|
|
106
|
+
|
|
107
|
+
# Get the ZIP file size and content
|
|
108
|
+
zip_buffer.seek(0)
|
|
109
|
+
zip_content = zip_buffer.getvalue()
|
|
110
|
+
zip_size = len(zip_content)
|
|
111
|
+
|
|
112
|
+
# Save to LogExportState - S3 upload happens here
|
|
113
|
+
filename = f'audit_logs_{from_date.strftime("%Y%m%d")}_{to_date.strftime("%Y%m%d")}.zip'
|
|
114
|
+
export_state.data.save(filename, ContentFile(zip_content), save=False)
|
|
115
|
+
export_state.size = zip_size
|
|
116
|
+
export_state.set_success()
|
|
117
|
+
|
|
118
|
+
return export_state
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
export_state.set_failed()
|
|
122
|
+
raise e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _serialize_log_entry(log) -> dict:
|
|
126
|
+
"""
|
|
127
|
+
Serialize a single LogEntry to a dictionary (optimized version).
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
log: LogEntry object
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
dict: Serialized log entry with minimal overhead
|
|
134
|
+
"""
|
|
135
|
+
return {
|
|
136
|
+
'id': log.id,
|
|
137
|
+
'timestamp': log.timestamp.isoformat() if log.timestamp else None,
|
|
138
|
+
'action': log.action,
|
|
139
|
+
'content_type': {
|
|
140
|
+
'app_label': log.content_type.app_label if log.content_type else None,
|
|
141
|
+
'model': log.content_type.model if log.content_type else None,
|
|
142
|
+
} if log.content_type else None,
|
|
143
|
+
'object_pk': log.object_pk,
|
|
144
|
+
'object_repr': log.object_repr,
|
|
145
|
+
'changes': log.changes,
|
|
146
|
+
'actor': {
|
|
147
|
+
'id': log.actor.id if log.actor else None,
|
|
148
|
+
'username': str(log.actor) if log.actor else None,
|
|
149
|
+
} if log.actor else None,
|
|
150
|
+
'remote_addr': log.remote_addr,
|
|
151
|
+
'additional_data': log.additional_data if hasattr(log, 'additional_data') else None,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def serialize_log_entries(queryset: QuerySet) -> list:
|
|
156
|
+
"""
|
|
157
|
+
Serialize LogEntry queryset to a list of dictionaries.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
queryset: QuerySet of LogEntry objects
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
list: List of serialized log entries
|
|
164
|
+
"""
|
|
165
|
+
return [_serialize_log_entry(log) for log in queryset]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def export_logs_since_last_export(batch_size: int = 5000) -> Optional[LogExportState]:
|
|
169
|
+
"""
|
|
170
|
+
Export logs since the last successful export.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
batch_size: Number of records to process at a time
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
LogExportState or None: The created export state, or None if no previous export exists
|
|
177
|
+
"""
|
|
178
|
+
# Get the last successful export
|
|
179
|
+
last_export = LogExportState.objects.filter(
|
|
180
|
+
status='success',
|
|
181
|
+
last_export_date__isnull=False
|
|
182
|
+
).order_by('-last_export_date').first()
|
|
183
|
+
|
|
184
|
+
if last_export:
|
|
185
|
+
from_date = last_export.last_export_date
|
|
186
|
+
else:
|
|
187
|
+
# If no previous export, start from the earliest log
|
|
188
|
+
LogEntry = get_logentry_model()
|
|
189
|
+
earliest_log = LogEntry.objects.order_by('timestamp').first()
|
|
190
|
+
|
|
191
|
+
if not earliest_log:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
from_date = earliest_log.timestamp
|
|
195
|
+
|
|
196
|
+
return export_logs_from_date(from_date=from_date, batch_size=batch_size)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_export_statistics() -> dict:
|
|
200
|
+
"""
|
|
201
|
+
Get statistics about log exports.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
dict: Statistics including total exports, successful exports, failed exports, etc.
|
|
205
|
+
"""
|
|
206
|
+
from oxutils.enums.audit import ExportStatus
|
|
207
|
+
|
|
208
|
+
total_exports = LogExportState.objects.count()
|
|
209
|
+
successful_exports = LogExportState.objects.filter(status=ExportStatus.SUCCESS).count()
|
|
210
|
+
failed_exports = LogExportState.objects.filter(status=ExportStatus.FAILED).count()
|
|
211
|
+
pending_exports = LogExportState.objects.filter(status=ExportStatus.PENDING).count()
|
|
212
|
+
|
|
213
|
+
last_export = LogExportState.objects.filter(
|
|
214
|
+
status=ExportStatus.SUCCESS
|
|
215
|
+
).order_by('-last_export_date').first()
|
|
216
|
+
|
|
217
|
+
total_size = sum(
|
|
218
|
+
export.size for export in LogExportState.objects.filter(status=ExportStatus.SUCCESS)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
'total_exports': total_exports,
|
|
223
|
+
'successful_exports': successful_exports,
|
|
224
|
+
'failed_exports': failed_exports,
|
|
225
|
+
'pending_exports': pending_exports,
|
|
226
|
+
'last_export_date': last_export.last_export_date if last_export else None,
|
|
227
|
+
'total_size_bytes': total_size,
|
|
228
|
+
'total_size_mb': round(total_size / (1024 * 1024), 2),
|
|
229
|
+
}
|
oxutils/audit/masks.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Auditlog masks
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def number_mask(value: str) -> str:
|
|
5
|
+
"""Mask a number showing only the last 4 digits.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
value: The number string to mask
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
Masked string with format: ****1234
|
|
12
|
+
"""
|
|
13
|
+
if not value:
|
|
14
|
+
return ""
|
|
15
|
+
|
|
16
|
+
value_str = str(value).strip()
|
|
17
|
+
if len(value_str) <= 4:
|
|
18
|
+
return "*" * len(value_str)
|
|
19
|
+
|
|
20
|
+
return "****" + value_str[-4:]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def phone_number_mask(value: str) -> str:
|
|
24
|
+
"""Mask a phone number showing only the last 4 digits.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
value: The phone number to mask
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Masked phone number with format: ****1234
|
|
31
|
+
"""
|
|
32
|
+
if not value:
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
# Remove common phone number formatting characters
|
|
36
|
+
cleaned = str(value).replace(" ", "").replace("-", "").replace("(", "").replace(")", "").replace("+", "")
|
|
37
|
+
|
|
38
|
+
if len(cleaned) <= 4:
|
|
39
|
+
return "*" * len(cleaned)
|
|
40
|
+
|
|
41
|
+
return "****" + cleaned[-4:]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def credit_card_mask(value: str) -> str:
|
|
45
|
+
"""Mask a credit card number showing only the last 4 digits.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
value: The credit card number to mask
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Masked credit card with format: ****1234
|
|
52
|
+
"""
|
|
53
|
+
if not value:
|
|
54
|
+
return ""
|
|
55
|
+
|
|
56
|
+
# Remove spaces and dashes from credit card number
|
|
57
|
+
cleaned = str(value).replace(" ", "").replace("-", "")
|
|
58
|
+
|
|
59
|
+
if len(cleaned) <= 4:
|
|
60
|
+
return "*" * len(cleaned)
|
|
61
|
+
|
|
62
|
+
return "****" + cleaned[-4:]
|
|
63
|
+
|
|
64
|
+
def email_mask(value: str) -> str:
|
|
65
|
+
"""Mask an email address showing only the first character and domain.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
value: The email address to mask
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Masked email with format: j***@example.com
|
|
72
|
+
"""
|
|
73
|
+
if not value:
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
value_str = str(value).strip()
|
|
77
|
+
|
|
78
|
+
if "@" not in value_str:
|
|
79
|
+
# Not a valid email format, mask most of it
|
|
80
|
+
if len(value_str) <= 2:
|
|
81
|
+
return "*" * len(value_str)
|
|
82
|
+
return value_str[0] + "*" * (len(value_str) - 1)
|
|
83
|
+
|
|
84
|
+
local, domain = value_str.rsplit("@", 1)
|
|
85
|
+
|
|
86
|
+
if not local:
|
|
87
|
+
return "***@" + domain
|
|
88
|
+
|
|
89
|
+
if len(local) == 1:
|
|
90
|
+
masked_local = "*"
|
|
91
|
+
elif len(local) == 2:
|
|
92
|
+
masked_local = local[0] + "*"
|
|
93
|
+
else:
|
|
94
|
+
masked_local = local[0] + "*" * (len(local) - 1)
|
|
95
|
+
|
|
96
|
+
return masked_local + "@" + domain
|
|
97
|
+
|
oxutils/audit/models.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from django.utils.translation import gettext_lazy as _
|
|
2
|
+
from django.utils import timezone
|
|
3
|
+
from django.db import models, transaction
|
|
4
|
+
from oxutils.enums.audit import ExportStatus
|
|
5
|
+
from oxutils.models.base import TimestampMixin
|
|
6
|
+
from oxutils.s3.storages import LogStorage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LogExportHistory(models.Model):
|
|
12
|
+
state = models.ForeignKey(
|
|
13
|
+
"LogExportState",
|
|
14
|
+
related_name='log_histories',
|
|
15
|
+
on_delete=models.CASCADE
|
|
16
|
+
)
|
|
17
|
+
status = models.CharField(
|
|
18
|
+
default=ExportStatus.PENDING,
|
|
19
|
+
choices=(
|
|
20
|
+
(ExportStatus.FAILED, _("Failed")),
|
|
21
|
+
(ExportStatus.PENDING, _('Pending')),
|
|
22
|
+
(ExportStatus.SUCCESS, _('Success'))
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
created_at = models.DateTimeField(
|
|
26
|
+
auto_now_add=True,
|
|
27
|
+
help_text="Date and time when this record was created"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LogExportState(TimestampMixin):
|
|
32
|
+
last_export_date = models.DateTimeField(null=True)
|
|
33
|
+
status = models.CharField(
|
|
34
|
+
default=ExportStatus.PENDING,
|
|
35
|
+
choices=(
|
|
36
|
+
(ExportStatus.FAILED, _("Failed")),
|
|
37
|
+
(ExportStatus.PENDING, _('Pending')),
|
|
38
|
+
(ExportStatus.SUCCESS, _('Success'))
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
data = models.FileField(storage=LogStorage())
|
|
42
|
+
size = models.BigIntegerField()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def create(cls, size: int = 0):
|
|
46
|
+
return cls.objects.create(
|
|
47
|
+
status=ExportStatus.PENDING,
|
|
48
|
+
size=size
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@transaction.atomic
|
|
52
|
+
def set_success(self):
|
|
53
|
+
self.status = ExportStatus.SUCCESS
|
|
54
|
+
self.last_export_date = timezone.now()
|
|
55
|
+
LogExportHistory.objects.create(
|
|
56
|
+
state=self,
|
|
57
|
+
status=ExportStatus.SUCCESS
|
|
58
|
+
)
|
|
59
|
+
self.save(update_fields=(
|
|
60
|
+
'status',
|
|
61
|
+
'last_export_date',
|
|
62
|
+
'updated_at'
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
@transaction.atomic
|
|
66
|
+
def set_failed(self):
|
|
67
|
+
self.status = ExportStatus.FAILED
|
|
68
|
+
self.save(update_fields=(
|
|
69
|
+
'status',
|
|
70
|
+
'updated_at'
|
|
71
|
+
))
|
|
72
|
+
LogExportHistory.objects.create(
|
|
73
|
+
state=self,
|
|
74
|
+
status=ExportStatus.FAILED
|
|
75
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Oxiliere Audit settings
|
|
2
|
+
|
|
3
|
+
AUDITLOG_DISABLE_REMOTE_ADDR = False
|
|
4
|
+
AUDITLOG_MASK_TRACKING_FIELDS = (
|
|
5
|
+
"password",
|
|
6
|
+
"api_key",
|
|
7
|
+
"secret_token",
|
|
8
|
+
"token",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
AUDITLOG_EXCLUDE_TRACKING_FIELDS = (
|
|
12
|
+
"created_at",
|
|
13
|
+
"updated_at",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
CID_GENERATE = False
|
|
17
|
+
|
|
18
|
+
AUDITLOG_CID_GETTER = "cid.locals.get_cid"
|
|
19
|
+
AUDITLOG_LOGENTRY_MODEL = "auditlog.LogEntry"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .base import celery_app
|
oxutils/celery/base.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import structlog
|
|
4
|
+
from celery import Celery
|
|
5
|
+
from celery.signals import setup_logging
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django_structlog.celery.steps import DjangoStructLogInitStep
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
celery_app = Celery(getattr(settings, "CELERY_APP_NAME", 'oxiliere_celery'))
|
|
14
|
+
|
|
15
|
+
celery_app.config_from_object('django.conf:settings', namespace='CELERY')
|
|
16
|
+
|
|
17
|
+
# A step to initialize django-structlog
|
|
18
|
+
celery_app.steps['worker'].add(DjangoStructLogInitStep)
|
|
19
|
+
|
|
20
|
+
celery_app.autodiscover_tasks()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@setup_logging.connect
|
|
24
|
+
def receiver_setup_logging(loglevel, logfile, format, colorize, **kwargs): # pragma: no cover
|
|
25
|
+
logging.config.dictConfig(
|
|
26
|
+
{
|
|
27
|
+
"version": 1,
|
|
28
|
+
"disable_existing_loggers": False,
|
|
29
|
+
"formatters": {
|
|
30
|
+
"json_formatter": {
|
|
31
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
32
|
+
"processor": structlog.processors.JSONRenderer(),
|
|
33
|
+
},
|
|
34
|
+
"plain_console": {
|
|
35
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
36
|
+
"processor": structlog.dev.ConsoleRenderer(),
|
|
37
|
+
},
|
|
38
|
+
"key_value": {
|
|
39
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
40
|
+
"processor": structlog.processors.KeyValueRenderer(
|
|
41
|
+
key_order=['timestamp', 'level', 'event', 'logger']
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
"handlers": {
|
|
46
|
+
"console": {
|
|
47
|
+
"class": "logging.StreamHandler",
|
|
48
|
+
"formatter": "plain_console",
|
|
49
|
+
'filters': ['correlation'],
|
|
50
|
+
},
|
|
51
|
+
"json_file": {
|
|
52
|
+
"class": "logging.handlers.WatchedFileHandler",
|
|
53
|
+
"filename": "logs/json.log",
|
|
54
|
+
"formatter": "json_formatter",
|
|
55
|
+
'filters': ['correlation'],
|
|
56
|
+
},
|
|
57
|
+
"flat_line_file": {
|
|
58
|
+
"class": "logging.handlers.WatchedFileHandler",
|
|
59
|
+
"filename": "logs/flat_line.log",
|
|
60
|
+
"formatter": "key_value",
|
|
61
|
+
'filters': ['correlation'],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
'filters': {
|
|
65
|
+
'correlation': {
|
|
66
|
+
'()': 'cid.log.CidContextFilter'
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
"loggers": {
|
|
70
|
+
"django_structlog": {
|
|
71
|
+
"handlers": ["console", "flat_line_file", "json_file"],
|
|
72
|
+
"level": "INFO",
|
|
73
|
+
},
|
|
74
|
+
"oxiliere_log": {
|
|
75
|
+
"handlers": ["console", "flat_line_file", "json_file"],
|
|
76
|
+
"level": "INFO",
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
structlog.configure(
|
|
83
|
+
processors=[
|
|
84
|
+
structlog.contextvars.merge_contextvars,
|
|
85
|
+
structlog.stdlib.filter_by_level,
|
|
86
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
87
|
+
structlog.stdlib.add_logger_name,
|
|
88
|
+
structlog.stdlib.add_log_level,
|
|
89
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
90
|
+
structlog.processors.StackInfoRenderer(),
|
|
91
|
+
structlog.processors.format_exc_info,
|
|
92
|
+
structlog.processors.UnicodeDecoder(),
|
|
93
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
94
|
+
],
|
|
95
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
96
|
+
cache_logger_on_first_use=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
CELERY_CACHE_BACKEND = 'default'
|
oxutils/conf.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
UTILS_APPS = (
|
|
2
|
+
'django_structlog',
|
|
3
|
+
'auditlog',
|
|
4
|
+
'cid.apps.CidAppConfig',
|
|
5
|
+
'django_celery_results',
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
AUDIT_MIDDLEWARE = (
|
|
9
|
+
'cid.middleware.CidMiddleware',
|
|
10
|
+
'auditlog.middleware.AuditlogMiddleware',
|
|
11
|
+
'django_structlog.middlewares.RequestMiddleware',
|
|
12
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .invoices import InvoiceStatusEnum
|
oxutils/enums/audit.py
ADDED