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.
Files changed (62) hide show
  1. truthound_dashboard/__init__.py +11 -0
  2. truthound_dashboard/__main__.py +6 -0
  3. truthound_dashboard/api/__init__.py +15 -0
  4. truthound_dashboard/api/deps.py +153 -0
  5. truthound_dashboard/api/drift.py +179 -0
  6. truthound_dashboard/api/error_handlers.py +287 -0
  7. truthound_dashboard/api/health.py +78 -0
  8. truthound_dashboard/api/history.py +62 -0
  9. truthound_dashboard/api/middleware.py +626 -0
  10. truthound_dashboard/api/notifications.py +561 -0
  11. truthound_dashboard/api/profile.py +52 -0
  12. truthound_dashboard/api/router.py +83 -0
  13. truthound_dashboard/api/rules.py +277 -0
  14. truthound_dashboard/api/schedules.py +329 -0
  15. truthound_dashboard/api/schemas.py +136 -0
  16. truthound_dashboard/api/sources.py +229 -0
  17. truthound_dashboard/api/validations.py +125 -0
  18. truthound_dashboard/cli.py +226 -0
  19. truthound_dashboard/config.py +132 -0
  20. truthound_dashboard/core/__init__.py +264 -0
  21. truthound_dashboard/core/base.py +185 -0
  22. truthound_dashboard/core/cache.py +479 -0
  23. truthound_dashboard/core/connections.py +331 -0
  24. truthound_dashboard/core/encryption.py +409 -0
  25. truthound_dashboard/core/exceptions.py +627 -0
  26. truthound_dashboard/core/logging.py +488 -0
  27. truthound_dashboard/core/maintenance.py +542 -0
  28. truthound_dashboard/core/notifications/__init__.py +56 -0
  29. truthound_dashboard/core/notifications/base.py +390 -0
  30. truthound_dashboard/core/notifications/channels.py +557 -0
  31. truthound_dashboard/core/notifications/dispatcher.py +453 -0
  32. truthound_dashboard/core/notifications/events.py +155 -0
  33. truthound_dashboard/core/notifications/service.py +744 -0
  34. truthound_dashboard/core/sampling.py +626 -0
  35. truthound_dashboard/core/scheduler.py +311 -0
  36. truthound_dashboard/core/services.py +1531 -0
  37. truthound_dashboard/core/truthound_adapter.py +659 -0
  38. truthound_dashboard/db/__init__.py +67 -0
  39. truthound_dashboard/db/base.py +108 -0
  40. truthound_dashboard/db/database.py +196 -0
  41. truthound_dashboard/db/models.py +732 -0
  42. truthound_dashboard/db/repository.py +237 -0
  43. truthound_dashboard/main.py +309 -0
  44. truthound_dashboard/schemas/__init__.py +150 -0
  45. truthound_dashboard/schemas/base.py +96 -0
  46. truthound_dashboard/schemas/drift.py +118 -0
  47. truthound_dashboard/schemas/history.py +74 -0
  48. truthound_dashboard/schemas/profile.py +91 -0
  49. truthound_dashboard/schemas/rule.py +199 -0
  50. truthound_dashboard/schemas/schedule.py +88 -0
  51. truthound_dashboard/schemas/schema.py +121 -0
  52. truthound_dashboard/schemas/source.py +138 -0
  53. truthound_dashboard/schemas/validation.py +192 -0
  54. truthound_dashboard/static/assets/index-BqJMyAHX.js +110 -0
  55. truthound_dashboard/static/assets/index-DMDxHCTs.js +465 -0
  56. truthound_dashboard/static/assets/index-Dm2D11TK.css +1 -0
  57. truthound_dashboard/static/index.html +15 -0
  58. truthound_dashboard/static/mockServiceWorker.js +349 -0
  59. truthound_dashboard-1.0.0.dist-info/METADATA +218 -0
  60. truthound_dashboard-1.0.0.dist-info/RECORD +62 -0
  61. truthound_dashboard-1.0.0.dist-info/WHEEL +4 -0
  62. 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