truthound-dashboard 1.0.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.
- truthound_dashboard/__init__.py +11 -0
- truthound_dashboard/__main__.py +6 -0
- truthound_dashboard/api/__init__.py +15 -0
- truthound_dashboard/api/deps.py +153 -0
- truthound_dashboard/api/drift.py +179 -0
- truthound_dashboard/api/error_handlers.py +287 -0
- truthound_dashboard/api/health.py +78 -0
- truthound_dashboard/api/history.py +62 -0
- truthound_dashboard/api/middleware.py +626 -0
- truthound_dashboard/api/notifications.py +561 -0
- truthound_dashboard/api/profile.py +52 -0
- truthound_dashboard/api/router.py +83 -0
- truthound_dashboard/api/rules.py +277 -0
- truthound_dashboard/api/schedules.py +329 -0
- truthound_dashboard/api/schemas.py +136 -0
- truthound_dashboard/api/sources.py +229 -0
- truthound_dashboard/api/validations.py +125 -0
- truthound_dashboard/cli.py +226 -0
- truthound_dashboard/config.py +132 -0
- truthound_dashboard/core/__init__.py +264 -0
- truthound_dashboard/core/base.py +185 -0
- truthound_dashboard/core/cache.py +479 -0
- truthound_dashboard/core/connections.py +331 -0
- truthound_dashboard/core/encryption.py +409 -0
- truthound_dashboard/core/exceptions.py +627 -0
- truthound_dashboard/core/logging.py +488 -0
- truthound_dashboard/core/maintenance.py +542 -0
- truthound_dashboard/core/notifications/__init__.py +56 -0
- truthound_dashboard/core/notifications/base.py +390 -0
- truthound_dashboard/core/notifications/channels.py +557 -0
- truthound_dashboard/core/notifications/dispatcher.py +453 -0
- truthound_dashboard/core/notifications/events.py +155 -0
- truthound_dashboard/core/notifications/service.py +744 -0
- truthound_dashboard/core/sampling.py +626 -0
- truthound_dashboard/core/scheduler.py +311 -0
- truthound_dashboard/core/services.py +1531 -0
- truthound_dashboard/core/truthound_adapter.py +659 -0
- truthound_dashboard/db/__init__.py +67 -0
- truthound_dashboard/db/base.py +108 -0
- truthound_dashboard/db/database.py +196 -0
- truthound_dashboard/db/models.py +732 -0
- truthound_dashboard/db/repository.py +237 -0
- truthound_dashboard/main.py +309 -0
- truthound_dashboard/schemas/__init__.py +150 -0
- truthound_dashboard/schemas/base.py +96 -0
- truthound_dashboard/schemas/drift.py +118 -0
- truthound_dashboard/schemas/history.py +74 -0
- truthound_dashboard/schemas/profile.py +91 -0
- truthound_dashboard/schemas/rule.py +199 -0
- truthound_dashboard/schemas/schedule.py +88 -0
- truthound_dashboard/schemas/schema.py +121 -0
- truthound_dashboard/schemas/source.py +138 -0
- truthound_dashboard/schemas/validation.py +192 -0
- truthound_dashboard/static/assets/index-BqJMyAHX.js +110 -0
- truthound_dashboard/static/assets/index-DMDxHCTs.js +465 -0
- truthound_dashboard/static/assets/index-Dm2D11TK.css +1 -0
- truthound_dashboard/static/index.html +15 -0
- truthound_dashboard/static/mockServiceWorker.js +349 -0
- truthound_dashboard-1.0.0.dist-info/METADATA +218 -0
- truthound_dashboard-1.0.0.dist-info/RECORD +62 -0
- truthound_dashboard-1.0.0.dist-info/WHEEL +4 -0
- truthound_dashboard-1.0.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""Logging configuration and utilities.
|
|
2
|
+
|
|
3
|
+
This module provides a comprehensive logging system with:
|
|
4
|
+
- Configurable log levels and formats
|
|
5
|
+
- File and console handlers
|
|
6
|
+
- Structured logging support
|
|
7
|
+
- Log rotation
|
|
8
|
+
- Context-aware logging
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
# Setup logging
|
|
12
|
+
setup_logging(level="INFO")
|
|
13
|
+
|
|
14
|
+
# Get logger
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
logger.info("Application started")
|
|
17
|
+
|
|
18
|
+
# Structured logging
|
|
19
|
+
logger.info("Request processed", extra={"request_id": "abc123", "duration_ms": 50})
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
import sys
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from truthound_dashboard.config import get_settings
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class LogConfig:
|
|
38
|
+
"""Configuration for logging.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
|
42
|
+
format: Log format string.
|
|
43
|
+
date_format: Date format string.
|
|
44
|
+
log_to_file: Whether to log to file.
|
|
45
|
+
log_dir: Directory for log files.
|
|
46
|
+
max_file_size: Maximum size per log file in bytes.
|
|
47
|
+
backup_count: Number of backup files to keep.
|
|
48
|
+
json_format: Use JSON format for structured logging.
|
|
49
|
+
include_thread: Include thread name in logs.
|
|
50
|
+
include_process: Include process ID in logs.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
level: str = "INFO"
|
|
54
|
+
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
55
|
+
date_format: str = "%Y-%m-%d %H:%M:%S"
|
|
56
|
+
log_to_file: bool = True
|
|
57
|
+
log_dir: Path | None = None
|
|
58
|
+
max_file_size: int = 10 * 1024 * 1024 # 10MB
|
|
59
|
+
backup_count: int = 5
|
|
60
|
+
json_format: bool = False
|
|
61
|
+
include_thread: bool = False
|
|
62
|
+
include_process: bool = False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class JsonFormatter(logging.Formatter):
|
|
66
|
+
"""JSON log formatter for structured logging.
|
|
67
|
+
|
|
68
|
+
Outputs logs in JSON format for easier parsing and analysis.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
include_thread: bool = False,
|
|
74
|
+
include_process: bool = False,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Initialize JSON formatter.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
include_thread: Include thread name in output.
|
|
80
|
+
include_process: Include process ID in output.
|
|
81
|
+
"""
|
|
82
|
+
super().__init__()
|
|
83
|
+
self._include_thread = include_thread
|
|
84
|
+
self._include_process = include_process
|
|
85
|
+
|
|
86
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
87
|
+
"""Format log record as JSON.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
record: Log record to format.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
JSON string.
|
|
94
|
+
"""
|
|
95
|
+
log_data = {
|
|
96
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
97
|
+
"level": record.levelname,
|
|
98
|
+
"logger": record.name,
|
|
99
|
+
"message": record.getMessage(),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Add location info
|
|
103
|
+
if record.pathname:
|
|
104
|
+
log_data["location"] = {
|
|
105
|
+
"file": record.filename,
|
|
106
|
+
"line": record.lineno,
|
|
107
|
+
"function": record.funcName,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Add thread/process info
|
|
111
|
+
if self._include_thread:
|
|
112
|
+
log_data["thread"] = record.threadName
|
|
113
|
+
|
|
114
|
+
if self._include_process:
|
|
115
|
+
log_data["process"] = record.process
|
|
116
|
+
|
|
117
|
+
# Add exception info
|
|
118
|
+
if record.exc_info:
|
|
119
|
+
log_data["exception"] = self.formatException(record.exc_info)
|
|
120
|
+
|
|
121
|
+
# Add extra fields
|
|
122
|
+
extra_keys = set(record.__dict__.keys()) - {
|
|
123
|
+
"name",
|
|
124
|
+
"msg",
|
|
125
|
+
"args",
|
|
126
|
+
"created",
|
|
127
|
+
"filename",
|
|
128
|
+
"funcName",
|
|
129
|
+
"levelname",
|
|
130
|
+
"levelno",
|
|
131
|
+
"lineno",
|
|
132
|
+
"module",
|
|
133
|
+
"msecs",
|
|
134
|
+
"pathname",
|
|
135
|
+
"process",
|
|
136
|
+
"processName",
|
|
137
|
+
"relativeCreated",
|
|
138
|
+
"stack_info",
|
|
139
|
+
"exc_info",
|
|
140
|
+
"exc_text",
|
|
141
|
+
"thread",
|
|
142
|
+
"threadName",
|
|
143
|
+
"message",
|
|
144
|
+
"taskName",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for key in extra_keys:
|
|
148
|
+
value = getattr(record, key)
|
|
149
|
+
if value is not None:
|
|
150
|
+
try:
|
|
151
|
+
# Ensure value is JSON serializable
|
|
152
|
+
json.dumps(value)
|
|
153
|
+
log_data[key] = value
|
|
154
|
+
except (TypeError, ValueError):
|
|
155
|
+
log_data[key] = str(value)
|
|
156
|
+
|
|
157
|
+
return json.dumps(log_data)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ColorFormatter(logging.Formatter):
|
|
161
|
+
"""Colored log formatter for console output.
|
|
162
|
+
|
|
163
|
+
Uses ANSI escape codes to colorize log levels.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
COLORS = {
|
|
167
|
+
"DEBUG": "\033[36m", # Cyan
|
|
168
|
+
"INFO": "\033[32m", # Green
|
|
169
|
+
"WARNING": "\033[33m", # Yellow
|
|
170
|
+
"ERROR": "\033[31m", # Red
|
|
171
|
+
"CRITICAL": "\033[35m", # Magenta
|
|
172
|
+
}
|
|
173
|
+
RESET = "\033[0m"
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
fmt: str | None = None,
|
|
178
|
+
datefmt: str | None = None,
|
|
179
|
+
use_colors: bool = True,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Initialize color formatter.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
fmt: Log format string.
|
|
185
|
+
datefmt: Date format string.
|
|
186
|
+
use_colors: Whether to use colors.
|
|
187
|
+
"""
|
|
188
|
+
super().__init__(fmt, datefmt)
|
|
189
|
+
self._use_colors = use_colors and sys.stdout.isatty()
|
|
190
|
+
|
|
191
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
192
|
+
"""Format log record with colors.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
record: Log record to format.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Formatted string with ANSI colors.
|
|
199
|
+
"""
|
|
200
|
+
if self._use_colors:
|
|
201
|
+
color = self.COLORS.get(record.levelname, "")
|
|
202
|
+
record.levelname = f"{color}{record.levelname}{self.RESET}"
|
|
203
|
+
|
|
204
|
+
return super().format(record)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class LoggerAdapter(logging.LoggerAdapter):
|
|
208
|
+
"""Logger adapter with additional context.
|
|
209
|
+
|
|
210
|
+
Allows adding persistent context to all log messages.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def __init__(
|
|
214
|
+
self,
|
|
215
|
+
logger: logging.Logger,
|
|
216
|
+
extra: dict[str, Any] | None = None,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Initialize logger adapter.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
logger: Base logger.
|
|
222
|
+
extra: Extra context to add to all messages.
|
|
223
|
+
"""
|
|
224
|
+
super().__init__(logger, extra or {})
|
|
225
|
+
|
|
226
|
+
def process(
|
|
227
|
+
self,
|
|
228
|
+
msg: str,
|
|
229
|
+
kwargs: dict[str, Any],
|
|
230
|
+
) -> tuple[str, dict[str, Any]]:
|
|
231
|
+
"""Process log message with extra context.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
msg: Log message.
|
|
235
|
+
kwargs: Keyword arguments.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Processed message and kwargs.
|
|
239
|
+
"""
|
|
240
|
+
extra = kwargs.get("extra", {})
|
|
241
|
+
extra.update(self.extra)
|
|
242
|
+
kwargs["extra"] = extra
|
|
243
|
+
return msg, kwargs
|
|
244
|
+
|
|
245
|
+
def with_context(self, **context: Any) -> LoggerAdapter:
|
|
246
|
+
"""Create new adapter with additional context.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
**context: Additional context to add.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
New LoggerAdapter with merged context.
|
|
253
|
+
"""
|
|
254
|
+
merged = {**self.extra, **context}
|
|
255
|
+
return LoggerAdapter(self.logger, merged)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def setup_logging(
|
|
259
|
+
config: LogConfig | None = None,
|
|
260
|
+
level: str | None = None,
|
|
261
|
+
) -> logging.Logger:
|
|
262
|
+
"""Configure application logging.
|
|
263
|
+
|
|
264
|
+
Sets up logging with console and optional file handlers.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
config: Logging configuration.
|
|
268
|
+
level: Override log level.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Root logger for the application.
|
|
272
|
+
"""
|
|
273
|
+
settings = get_settings()
|
|
274
|
+
config = config or LogConfig()
|
|
275
|
+
|
|
276
|
+
# Override level if specified
|
|
277
|
+
if level:
|
|
278
|
+
config.level = level.upper()
|
|
279
|
+
|
|
280
|
+
# Set log directory
|
|
281
|
+
if config.log_dir is None:
|
|
282
|
+
config.log_dir = settings.data_dir / "logs"
|
|
283
|
+
|
|
284
|
+
# Create log directory
|
|
285
|
+
config.log_dir.mkdir(parents=True, exist_ok=True)
|
|
286
|
+
|
|
287
|
+
# Get root logger
|
|
288
|
+
root_logger = logging.getLogger("truthound_dashboard")
|
|
289
|
+
root_logger.setLevel(getattr(logging, config.level))
|
|
290
|
+
|
|
291
|
+
# Remove existing handlers
|
|
292
|
+
root_logger.handlers.clear()
|
|
293
|
+
|
|
294
|
+
# Console handler
|
|
295
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
296
|
+
console_handler.setLevel(getattr(logging, config.level))
|
|
297
|
+
|
|
298
|
+
if config.json_format:
|
|
299
|
+
console_formatter = JsonFormatter(
|
|
300
|
+
include_thread=config.include_thread,
|
|
301
|
+
include_process=config.include_process,
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
console_formatter = ColorFormatter(
|
|
305
|
+
fmt=config.format,
|
|
306
|
+
datefmt=config.date_format,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
console_handler.setFormatter(console_formatter)
|
|
310
|
+
root_logger.addHandler(console_handler)
|
|
311
|
+
|
|
312
|
+
# File handler
|
|
313
|
+
if config.log_to_file:
|
|
314
|
+
log_file = config.log_dir / "dashboard.log"
|
|
315
|
+
|
|
316
|
+
file_handler = RotatingFileHandler(
|
|
317
|
+
log_file,
|
|
318
|
+
maxBytes=config.max_file_size,
|
|
319
|
+
backupCount=config.backup_count,
|
|
320
|
+
)
|
|
321
|
+
file_handler.setLevel(getattr(logging, config.level))
|
|
322
|
+
|
|
323
|
+
if config.json_format:
|
|
324
|
+
file_formatter = JsonFormatter(
|
|
325
|
+
include_thread=config.include_thread,
|
|
326
|
+
include_process=config.include_process,
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
file_formatter = logging.Formatter(
|
|
330
|
+
fmt=config.format,
|
|
331
|
+
datefmt=config.date_format,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
file_handler.setFormatter(file_formatter)
|
|
335
|
+
root_logger.addHandler(file_handler)
|
|
336
|
+
|
|
337
|
+
# Configure third-party loggers
|
|
338
|
+
configure_library_loggers(config.level)
|
|
339
|
+
|
|
340
|
+
root_logger.info(f"Logging configured at {config.level} level")
|
|
341
|
+
return root_logger
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def configure_library_loggers(level: str) -> None:
|
|
345
|
+
"""Configure logging levels for third-party libraries.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
level: Application log level.
|
|
349
|
+
"""
|
|
350
|
+
# Quiet down noisy libraries
|
|
351
|
+
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
|
352
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
353
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
354
|
+
|
|
355
|
+
# Enable SQLAlchemy logging in debug mode
|
|
356
|
+
if level == "DEBUG":
|
|
357
|
+
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
|
|
358
|
+
else:
|
|
359
|
+
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def get_logger(name: str, **context: Any) -> LoggerAdapter:
|
|
363
|
+
"""Get a logger with optional context.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
name: Logger name (typically __name__).
|
|
367
|
+
**context: Additional context to add to all messages.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
LoggerAdapter instance.
|
|
371
|
+
"""
|
|
372
|
+
logger = logging.getLogger(name)
|
|
373
|
+
return LoggerAdapter(logger, context)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class AuditLogger:
|
|
377
|
+
"""Specialized logger for audit events.
|
|
378
|
+
|
|
379
|
+
Logs security and compliance-relevant events in a structured format.
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
def __init__(self) -> None:
|
|
383
|
+
"""Initialize audit logger."""
|
|
384
|
+
self._logger = logging.getLogger("truthound_dashboard.audit")
|
|
385
|
+
|
|
386
|
+
def log_event(
|
|
387
|
+
self,
|
|
388
|
+
event_type: str,
|
|
389
|
+
user: str | None = None,
|
|
390
|
+
resource: str | None = None,
|
|
391
|
+
action: str | None = None,
|
|
392
|
+
status: str = "success",
|
|
393
|
+
details: dict[str, Any] | None = None,
|
|
394
|
+
) -> None:
|
|
395
|
+
"""Log an audit event.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
event_type: Type of event (auth, access, modification, etc.).
|
|
399
|
+
user: User who performed the action.
|
|
400
|
+
resource: Resource affected.
|
|
401
|
+
action: Action performed.
|
|
402
|
+
status: Result status (success, failure).
|
|
403
|
+
details: Additional event details.
|
|
404
|
+
"""
|
|
405
|
+
event = {
|
|
406
|
+
"event_type": event_type,
|
|
407
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
408
|
+
"user": user or "system",
|
|
409
|
+
"resource": resource,
|
|
410
|
+
"action": action,
|
|
411
|
+
"status": status,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if details:
|
|
415
|
+
event["details"] = details
|
|
416
|
+
|
|
417
|
+
self._logger.info(
|
|
418
|
+
f"AUDIT: {event_type} - {action} on {resource}",
|
|
419
|
+
extra=event,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
def log_auth_success(self, user: str, method: str) -> None:
|
|
423
|
+
"""Log successful authentication."""
|
|
424
|
+
self.log_event(
|
|
425
|
+
event_type="auth",
|
|
426
|
+
user=user,
|
|
427
|
+
action="login",
|
|
428
|
+
status="success",
|
|
429
|
+
details={"method": method},
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def log_auth_failure(self, user: str, reason: str) -> None:
|
|
433
|
+
"""Log failed authentication."""
|
|
434
|
+
self.log_event(
|
|
435
|
+
event_type="auth",
|
|
436
|
+
user=user,
|
|
437
|
+
action="login",
|
|
438
|
+
status="failure",
|
|
439
|
+
details={"reason": reason},
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def log_resource_access(
|
|
443
|
+
self,
|
|
444
|
+
user: str,
|
|
445
|
+
resource: str,
|
|
446
|
+
action: str,
|
|
447
|
+
granted: bool,
|
|
448
|
+
) -> None:
|
|
449
|
+
"""Log resource access attempt."""
|
|
450
|
+
self.log_event(
|
|
451
|
+
event_type="access",
|
|
452
|
+
user=user,
|
|
453
|
+
resource=resource,
|
|
454
|
+
action=action,
|
|
455
|
+
status="granted" if granted else "denied",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def log_data_modification(
|
|
459
|
+
self,
|
|
460
|
+
user: str,
|
|
461
|
+
resource: str,
|
|
462
|
+
action: str,
|
|
463
|
+
changes: dict[str, Any] | None = None,
|
|
464
|
+
) -> None:
|
|
465
|
+
"""Log data modification."""
|
|
466
|
+
self.log_event(
|
|
467
|
+
event_type="modification",
|
|
468
|
+
user=user,
|
|
469
|
+
resource=resource,
|
|
470
|
+
action=action,
|
|
471
|
+
details={"changes": changes} if changes else None,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# Singleton audit logger
|
|
476
|
+
_audit_logger: AuditLogger | None = None
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def get_audit_logger() -> AuditLogger:
|
|
480
|
+
"""Get audit logger singleton.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
AuditLogger instance.
|
|
484
|
+
"""
|
|
485
|
+
global _audit_logger
|
|
486
|
+
if _audit_logger is None:
|
|
487
|
+
_audit_logger = AuditLogger()
|
|
488
|
+
return _audit_logger
|