miso-client 0.1.0__py3-none-any.whl → 3.7.2__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 (69) hide show
  1. miso_client/__init__.py +523 -130
  2. miso_client/api/__init__.py +35 -0
  3. miso_client/api/auth_api.py +367 -0
  4. miso_client/api/logs_api.py +91 -0
  5. miso_client/api/permissions_api.py +88 -0
  6. miso_client/api/roles_api.py +88 -0
  7. miso_client/api/types/__init__.py +75 -0
  8. miso_client/api/types/auth_types.py +183 -0
  9. miso_client/api/types/logs_types.py +71 -0
  10. miso_client/api/types/permissions_types.py +31 -0
  11. miso_client/api/types/roles_types.py +31 -0
  12. miso_client/errors.py +30 -4
  13. miso_client/models/__init__.py +4 -0
  14. miso_client/models/config.py +275 -72
  15. miso_client/models/error_response.py +39 -0
  16. miso_client/models/filter.py +255 -0
  17. miso_client/models/pagination.py +44 -0
  18. miso_client/models/sort.py +25 -0
  19. miso_client/services/__init__.py +6 -5
  20. miso_client/services/auth.py +496 -87
  21. miso_client/services/cache.py +42 -41
  22. miso_client/services/encryption.py +18 -17
  23. miso_client/services/logger.py +467 -328
  24. miso_client/services/logger_chain.py +288 -0
  25. miso_client/services/permission.py +130 -67
  26. miso_client/services/redis.py +28 -23
  27. miso_client/services/role.py +145 -62
  28. miso_client/utils/__init__.py +3 -3
  29. miso_client/utils/audit_log_queue.py +222 -0
  30. miso_client/utils/auth_strategy.py +88 -0
  31. miso_client/utils/auth_utils.py +65 -0
  32. miso_client/utils/circuit_breaker.py +125 -0
  33. miso_client/utils/client_token_manager.py +244 -0
  34. miso_client/utils/config_loader.py +88 -17
  35. miso_client/utils/controller_url_resolver.py +80 -0
  36. miso_client/utils/data_masker.py +104 -33
  37. miso_client/utils/environment_token.py +126 -0
  38. miso_client/utils/error_utils.py +216 -0
  39. miso_client/utils/fastapi_endpoints.py +166 -0
  40. miso_client/utils/filter.py +364 -0
  41. miso_client/utils/filter_applier.py +143 -0
  42. miso_client/utils/filter_parser.py +110 -0
  43. miso_client/utils/flask_endpoints.py +169 -0
  44. miso_client/utils/http_client.py +494 -262
  45. miso_client/utils/http_client_logging.py +352 -0
  46. miso_client/utils/http_client_logging_helpers.py +197 -0
  47. miso_client/utils/http_client_query_helpers.py +138 -0
  48. miso_client/utils/http_error_handler.py +92 -0
  49. miso_client/utils/http_log_formatter.py +115 -0
  50. miso_client/utils/http_log_masker.py +203 -0
  51. miso_client/utils/internal_http_client.py +435 -0
  52. miso_client/utils/jwt_tools.py +125 -16
  53. miso_client/utils/logger_helpers.py +206 -0
  54. miso_client/utils/logging_helpers.py +70 -0
  55. miso_client/utils/origin_validator.py +128 -0
  56. miso_client/utils/pagination.py +275 -0
  57. miso_client/utils/request_context.py +285 -0
  58. miso_client/utils/sensitive_fields_loader.py +116 -0
  59. miso_client/utils/sort.py +116 -0
  60. miso_client/utils/token_utils.py +114 -0
  61. miso_client/utils/url_validator.py +66 -0
  62. miso_client/utils/user_token_refresh.py +245 -0
  63. miso_client-3.7.2.dist-info/METADATA +1021 -0
  64. miso_client-3.7.2.dist-info/RECORD +68 -0
  65. miso_client-0.1.0.dist-info/METADATA +0 -551
  66. miso_client-0.1.0.dist-info/RECORD +0 -23
  67. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
  68. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
  69. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/top_level.txt +0 -0
@@ -2,200 +2,135 @@
2
2
  Logger service for application logging and audit events.
3
3
 
4
4
  This module provides structured logging with Redis queuing and HTTP fallback.
5
- Includes JWT context extraction, data masking, correlation IDs, and performance metrics.
5
+ Includes JWT context extraction, data masking, and correlation IDs.
6
6
  """
7
7
 
8
- import os
8
+ import inspect
9
9
  import random
10
- import sys
11
10
  from datetime import datetime
12
- from typing import Optional, Dict, Any, Literal
13
- from ..models.config import (
14
- LogEntry,
15
- ClientLoggingOptions
16
- )
11
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, cast
12
+
13
+ if TYPE_CHECKING:
14
+ # Avoid import at runtime for frameworks not installed
15
+ pass
16
+
17
+ from ..models.config import ClientLoggingOptions, LogEntry
17
18
  from ..services.redis import RedisService
18
- from ..utils.http_client import HttpClient
19
- from ..utils.data_masker import DataMasker
20
- from ..utils.jwt_tools import decode_token
19
+ from ..utils.audit_log_queue import AuditLogQueue
20
+ from ..utils.circuit_breaker import CircuitBreaker
21
+ from ..utils.internal_http_client import InternalHttpClient
22
+ from ..utils.logger_helpers import build_log_entry, extract_metadata
23
+
24
+ if TYPE_CHECKING:
25
+ from ..api import ApiClient
26
+ from ..api.types.logs_types import LogRequest
27
+
28
+ # Import LoggerChain at runtime to avoid circular dependency
29
+ from .logger_chain import LoggerChain
21
30
 
22
31
 
23
32
  class LoggerService:
24
33
  """Logger service for application logging and audit events."""
25
-
26
- def __init__(self, http_client: HttpClient, redis: RedisService):
34
+
35
+ def __init__(
36
+ self,
37
+ internal_http_client: InternalHttpClient,
38
+ redis: RedisService,
39
+ http_client: Optional[Any] = None,
40
+ api_client: Optional["ApiClient"] = None,
41
+ ):
27
42
  """
28
43
  Initialize logger service.
29
-
44
+
30
45
  Args:
31
- http_client: HTTP client instance
46
+ internal_http_client: Internal HTTP client instance (used for log sending)
32
47
  redis: Redis service instance
48
+ http_client: Optional HttpClient instance for audit log queue (if available)
49
+ api_client: Optional API client instance (for typed API calls, use with caution to avoid circular dependency)
33
50
  """
34
- self.config = http_client.config
35
- self.http_client = http_client
51
+ self.config = internal_http_client.config
52
+ self.internal_http_client = internal_http_client
36
53
  self.redis = redis
54
+ self.api_client = api_client
37
55
  self.mask_sensitive_data = True # Default: mask sensitive data
38
56
  self.correlation_counter = 0
39
- self.performance_metrics: Dict[str, Dict[str, Any]] = {}
40
-
57
+ self.audit_log_queue: Optional[AuditLogQueue] = None
58
+
59
+ # Initialize circuit breaker for HTTP logging
60
+ circuit_breaker_config = self.config.audit.circuitBreaker if self.config.audit else None
61
+ self.circuit_breaker = CircuitBreaker(circuit_breaker_config)
62
+
63
+ # Event emission mode: list of callbacks for log events
64
+ # Callbacks receive (log_entry: LogEntry) as argument
65
+ self._event_listeners: List[Callable[[LogEntry], None]] = []
66
+
67
+ # Audit log queue will be initialized later by MisoClient after http_client is created
68
+ # This avoids circular dependency issues
69
+
41
70
  def set_masking(self, enabled: bool) -> None:
42
71
  """
43
72
  Enable or disable sensitive data masking.
44
-
73
+
45
74
  Args:
46
75
  enabled: Whether to enable data masking
47
76
  """
48
77
  self.mask_sensitive_data = enabled
49
-
78
+
79
+ def on(self, callback: Callable[[LogEntry], None]) -> None:
80
+ """
81
+ Register an event listener for log events.
82
+
83
+ When `emit_events=True` in config, logs are emitted as events instead of
84
+ being sent via HTTP/Redis. Registered callbacks receive LogEntry objects.
85
+
86
+ Args:
87
+ callback: Async or sync function that receives LogEntry as argument
88
+
89
+ Example:
90
+ >>> async def log_handler(log_entry: LogEntry):
91
+ ... print(f"Log: {log_entry.level} - {log_entry.message}")
92
+ >>> logger.on(log_handler)
93
+ """
94
+ if callback not in self._event_listeners:
95
+ self._event_listeners.append(callback)
96
+
97
+ def off(self, callback: Callable[[LogEntry], None]) -> None:
98
+ """
99
+ Unregister an event listener.
100
+
101
+ Args:
102
+ callback: Callback function to remove from listeners
103
+ """
104
+ if callback in self._event_listeners:
105
+ self._event_listeners.remove(callback)
106
+
50
107
  def _generate_correlation_id(self) -> str:
51
108
  """
52
109
  Generate unique correlation ID for request tracking.
53
-
110
+
54
111
  Format: {clientId[0:10]}-{timestamp}-{counter}-{random}
55
-
112
+
56
113
  Returns:
57
114
  Correlation ID string
58
115
  """
59
116
  self.correlation_counter = (self.correlation_counter + 1) % 10000
60
117
  timestamp = int(datetime.now().timestamp() * 1000)
61
- random_part = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=6))
62
- client_prefix = self.config.client_id[:10] if len(self.config.client_id) > 10 else self.config.client_id
118
+ random_part = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=6))
119
+ client_prefix = (
120
+ self.config.client_id[:10] if len(self.config.client_id) > 10 else self.config.client_id
121
+ )
63
122
  return f"{client_prefix}-{timestamp}-{self.correlation_counter}-{random_part}"
64
-
65
- def _extract_jwt_context(self, token: Optional[str]) -> Dict[str, Any]:
66
- """
67
- Extract JWT token information.
68
-
69
- Args:
70
- token: JWT token string
71
-
72
- Returns:
73
- Dictionary with userId, applicationId, sessionId, roles, permissions
74
- """
75
- if not token:
76
- return {}
77
-
78
- try:
79
- decoded = decode_token(token)
80
- if not decoded:
81
- return {}
82
-
83
- # Extract roles - handle different formats
84
- roles = []
85
- if "roles" in decoded:
86
- roles = decoded["roles"] if isinstance(decoded["roles"], list) else []
87
- elif "realm_access" in decoded and isinstance(decoded["realm_access"], dict):
88
- roles = decoded["realm_access"].get("roles", [])
89
-
90
- # Extract permissions - handle different formats
91
- permissions = []
92
- if "permissions" in decoded:
93
- permissions = decoded["permissions"] if isinstance(decoded["permissions"], list) else []
94
- elif "scope" in decoded and isinstance(decoded["scope"], str):
95
- permissions = decoded["scope"].split()
96
-
97
- return {
98
- "userId": decoded.get("sub") or decoded.get("userId") or decoded.get("user_id"),
99
- "applicationId": decoded.get("applicationId") or decoded.get("app_id"),
100
- "sessionId": decoded.get("sessionId") or decoded.get("sid"),
101
- "roles": roles,
102
- "permissions": permissions,
103
- }
104
- except Exception:
105
- # JWT parsing failed, return empty context
106
- return {}
107
-
108
- def _extract_metadata(self) -> Dict[str, Any]:
109
- """
110
- Extract metadata from environment (browser or Node.js).
111
-
112
- Returns:
113
- Dictionary with hostname, userAgent, etc.
114
- """
115
- metadata: Dict[str, Any] = {}
116
-
117
- # Try to extract Node.js/Python metadata
118
- if hasattr(os, "environ"):
119
- metadata["hostname"] = os.environ.get("HOSTNAME", "unknown")
120
-
121
- # In Python, we don't have browser metadata like in TypeScript
122
- # But we can capture some environment info
123
- metadata["platform"] = sys.platform
124
- metadata["python_version"] = sys.version
125
-
126
- return metadata
127
-
128
- def start_performance_tracking(self, operation_id: str) -> None:
129
- """
130
- Start performance tracking.
131
-
132
- Args:
133
- operation_id: Unique identifier for this operation
134
- """
135
- try:
136
- import psutil
137
- process = psutil.Process()
138
- memory_info = process.memory_info()
139
- memory_usage = {
140
- "rss": memory_info.rss,
141
- "heapTotal": memory_info.rss, # Approximation
142
- "heapUsed": memory_info.rss - memory_info.available if hasattr(memory_info, "available") else memory_info.rss,
143
- "external": 0,
144
- "arrayBuffers": 0,
145
- }
146
- except ImportError:
147
- # psutil not available
148
- memory_usage = None
149
-
150
- self.performance_metrics[operation_id] = {
151
- "startTime": int(datetime.now().timestamp() * 1000),
152
- "memoryUsage": memory_usage,
153
- }
154
-
155
- def end_performance_tracking(self, operation_id: str) -> Optional[Dict[str, Any]]:
156
- """
157
- End performance tracking and get metrics.
158
-
159
- Args:
160
- operation_id: Unique identifier for this operation
161
-
162
- Returns:
163
- Performance metrics dictionary or None if not found
164
- """
165
- if operation_id not in self.performance_metrics:
166
- return None
167
-
168
- metrics = self.performance_metrics[operation_id]
169
- metrics["endTime"] = int(datetime.now().timestamp() * 1000)
170
- metrics["duration"] = metrics["endTime"] - metrics["startTime"]
171
-
172
- try:
173
- import psutil
174
- process = psutil.Process()
175
- memory_info = process.memory_info()
176
- metrics["memoryUsage"] = {
177
- "rss": memory_info.rss,
178
- "heapTotal": memory_info.rss,
179
- "heapUsed": memory_info.rss - memory_info.available if hasattr(memory_info, "available") else memory_info.rss,
180
- "external": 0,
181
- "arrayBuffers": 0,
182
- }
183
- except (ImportError, Exception):
184
- pass # psutil not available or error getting memory info
185
-
186
- del self.performance_metrics[operation_id]
187
- return metrics
188
-
123
+
189
124
  async def error(
190
125
  self,
191
126
  message: str,
192
127
  context: Optional[Dict[str, Any]] = None,
193
128
  stack_trace: Optional[str] = None,
194
- options: Optional[ClientLoggingOptions] = None
129
+ options: Optional[ClientLoggingOptions] = None,
195
130
  ) -> None:
196
131
  """
197
132
  Log error message with optional stack trace and enhanced options.
198
-
133
+
199
134
  Args:
200
135
  message: Error message
201
136
  context: Additional context data
@@ -203,55 +138,51 @@ class LoggerService:
203
138
  options: Logging options
204
139
  """
205
140
  await self._log("error", message, context, stack_trace, options)
206
-
141
+
207
142
  async def audit(
208
143
  self,
209
144
  action: str,
210
145
  resource: str,
211
146
  context: Optional[Dict[str, Any]] = None,
212
- options: Optional[ClientLoggingOptions] = None
147
+ options: Optional[ClientLoggingOptions] = None,
213
148
  ) -> None:
214
149
  """
215
150
  Log audit event with enhanced options.
216
-
151
+
217
152
  Args:
218
153
  action: Action performed
219
154
  resource: Resource affected
220
155
  context: Additional context data
221
156
  options: Logging options
222
157
  """
223
- audit_context = {
224
- "action": action,
225
- "resource": resource,
226
- **(context or {})
227
- }
158
+ audit_context = {"action": action, "resource": resource, **(context or {})}
228
159
  await self._log("audit", f"Audit: {action} on {resource}", audit_context, None, options)
229
-
160
+
230
161
  async def info(
231
162
  self,
232
163
  message: str,
233
164
  context: Optional[Dict[str, Any]] = None,
234
- options: Optional[ClientLoggingOptions] = None
165
+ options: Optional[ClientLoggingOptions] = None,
235
166
  ) -> None:
236
167
  """
237
168
  Log info message with enhanced options.
238
-
169
+
239
170
  Args:
240
171
  message: Info message
241
172
  context: Additional context data
242
173
  options: Logging options
243
174
  """
244
175
  await self._log("info", message, context, None, options)
245
-
176
+
246
177
  async def debug(
247
178
  self,
248
179
  message: str,
249
180
  context: Optional[Dict[str, Any]] = None,
250
- options: Optional[ClientLoggingOptions] = None
181
+ options: Optional[ClientLoggingOptions] = None,
251
182
  ) -> None:
252
183
  """
253
184
  Log debug message with enhanced options.
254
-
185
+
255
186
  Args:
256
187
  message: Debug message
257
188
  context: Additional context data
@@ -259,18 +190,101 @@ class LoggerService:
259
190
  """
260
191
  if self.config.log_level == "debug":
261
192
  await self._log("debug", message, context, None, options)
262
-
193
+
194
+ async def _emit_log_event(self, log_entry: LogEntry) -> bool:
195
+ """
196
+ Emit log entry as event if event emission is enabled.
197
+
198
+ Args:
199
+ log_entry: LogEntry to emit
200
+
201
+ Returns:
202
+ True if event was emitted, False otherwise
203
+ """
204
+ if not (self.config.emit_events and self._event_listeners):
205
+ return False
206
+
207
+ for callback in self._event_listeners:
208
+ try:
209
+ if inspect.iscoroutinefunction(callback):
210
+ await callback(log_entry)
211
+ else:
212
+ callback(log_entry)
213
+ except Exception:
214
+ # Silently fail to avoid breaking application flow
215
+ pass
216
+ return True
217
+
218
+ async def _queue_audit_log(self, log_entry: LogEntry) -> bool:
219
+ """
220
+ Queue audit log entry if audit queue is available.
221
+
222
+ Args:
223
+ log_entry: LogEntry to queue
224
+
225
+ Returns:
226
+ True if queued, False otherwise
227
+ """
228
+ if log_entry.level == "audit" and self.audit_log_queue:
229
+ await self.audit_log_queue.add(log_entry)
230
+ return True
231
+ return False
232
+
233
+ async def _queue_redis_log(self, log_entry: LogEntry) -> bool:
234
+ """
235
+ Queue log entry in Redis if available.
236
+
237
+ Args:
238
+ log_entry: LogEntry to queue
239
+
240
+ Returns:
241
+ True if queued, False otherwise
242
+ """
243
+ if not self.redis.is_connected():
244
+ return False
245
+
246
+ queue_name = f"logs:{self.config.client_id}"
247
+ success = await self.redis.rpush(queue_name, log_entry.model_dump_json())
248
+ return success
249
+
250
+ async def _send_http_log(self, log_entry: LogEntry) -> None:
251
+ """
252
+ Send log entry via HTTP to controller.
253
+
254
+ Args:
255
+ log_entry: LogEntry to send
256
+ """
257
+ # Check circuit breaker before attempting HTTP logging
258
+ if self.circuit_breaker.is_open():
259
+ return
260
+
261
+ try:
262
+ if self.api_client:
263
+ log_request = self._transform_log_entry_to_request(log_entry)
264
+ await self.api_client.logs.send_log(log_request)
265
+ else:
266
+ log_payload = log_entry.model_dump(
267
+ exclude={"environment", "application"}, exclude_none=True
268
+ )
269
+ await self.internal_http_client.request("POST", "/api/v1/logs", log_payload)
270
+ self.circuit_breaker.record_success()
271
+ except Exception:
272
+ # Failed to send log to controller
273
+ self.circuit_breaker.record_failure()
274
+ # Silently fail to avoid infinite logging loops
275
+ pass
276
+
263
277
  async def _log(
264
278
  self,
265
279
  level: Literal["error", "audit", "info", "debug"],
266
280
  message: str,
267
281
  context: Optional[Dict[str, Any]] = None,
268
282
  stack_trace: Optional[str] = None,
269
- options: Optional[ClientLoggingOptions] = None
283
+ options: Optional[ClientLoggingOptions] = None,
270
284
  ) -> None:
271
285
  """
272
- Internal log method with enhanced features.
273
-
286
+ Core logging method with Redis queuing and HTTP fallback.
287
+
274
288
  Args:
275
289
  level: Log level
276
290
  message: Log message
@@ -278,180 +292,305 @@ class LoggerService:
278
292
  stack_trace: Stack trace for errors
279
293
  options: Logging options
280
294
  """
281
- # Extract JWT context if token provided
282
- jwt_context = self._extract_jwt_context(options.token if options else None) if options else {}
283
-
284
- # Extract environment metadata
285
- metadata = self._extract_metadata()
286
-
287
- # Generate correlation ID if not provided
288
- correlation_id = (options.correlationId if options else None) or self._generate_correlation_id()
289
-
290
- # Mask sensitive data in context if enabled
291
- mask_sensitive = (options.maskSensitiveData if options else None) is not False and self.mask_sensitive_data
292
- masked_context = (
293
- DataMasker.mask_sensitive_data(context) if mask_sensitive and context
294
- else context
295
+ # Build log entry
296
+ correlation_id = (
297
+ options.correlationId if options else None
298
+ ) or self._generate_correlation_id()
299
+
300
+ log_entry = build_log_entry(
301
+ level=level,
302
+ message=message,
303
+ context=context,
304
+ config_client_id=self.config.client_id,
305
+ correlation_id=correlation_id,
306
+ jwt_token=options.token if options else None,
307
+ stack_trace=stack_trace,
308
+ options=options,
309
+ metadata=extract_metadata(),
310
+ mask_sensitive=self.mask_sensitive_data,
295
311
  )
296
-
297
- # Add performance metrics if requested
298
- enhanced_context = masked_context
299
- if options and options.performanceMetrics:
300
- try:
301
- import psutil
302
- process = psutil.Process()
303
- memory_info = process.memory_info()
304
- enhanced_context = {
305
- **(enhanced_context or {}),
306
- "performance": {
307
- "memoryUsage": {
308
- "rss": memory_info.rss,
309
- "heapTotal": memory_info.rss,
310
- "heapUsed": memory_info.rss - memory_info.available if hasattr(memory_info, "available") else memory_info.rss,
311
- },
312
- "uptime": psutil.boot_time() if hasattr(psutil, "boot_time") else 0,
313
- }
314
- }
315
- except (ImportError, Exception):
316
- pass # psutil not available or error getting memory info
317
-
318
- log_entry_data = {
319
- "timestamp": datetime.utcnow().isoformat(),
320
- "level": level,
321
- "environment": "unknown", # Backend extracts from client credentials
322
- "application": self.config.client_id, # Use clientId as application identifier
323
- "applicationId": options.applicationId if options else None,
324
- "message": message,
325
- "context": enhanced_context,
326
- "stackTrace": stack_trace,
327
- "correlationId": correlation_id,
328
- "userId": (options.userId if options else None) or jwt_context.get("userId"),
329
- "sessionId": (options.sessionId if options else None) or jwt_context.get("sessionId"),
330
- "requestId": options.requestId if options else None,
331
- **metadata
332
- }
333
-
334
- # Remove None values
335
- log_entry_data = {k: v for k, v in log_entry_data.items() if v is not None}
336
-
337
- log_entry = LogEntry(**log_entry_data)
338
-
312
+
313
+ # Event emission mode: emit events instead of sending via HTTP/Redis
314
+ if await self._emit_log_event(log_entry):
315
+ return
316
+
317
+ # Use batch queue for audit logs if available
318
+ if await self._queue_audit_log(log_entry):
319
+ return
320
+
339
321
  # Try Redis first (if available)
340
- if self.redis.is_connected():
341
- queue_name = f"logs:{self.config.client_id}"
342
- success = await self.redis.rpush(queue_name, log_entry.model_dump_json())
343
-
344
- if success:
345
- return # Successfully queued in Redis
346
-
347
- # Fallback to unified logging endpoint with client credentials
348
- try:
349
- # Backend extracts environment and application from client credentials
350
- log_payload = log_entry.model_dump(exclude={"environment", "application"}, exclude_none=True)
351
- await self.http_client.request("POST", "/api/logs", log_payload)
352
- except Exception:
353
- # Failed to send log to controller
354
- # Silently fail to avoid infinite logging loops
355
- # Application should implement retry or buffer strategy if needed
356
- pass
357
-
322
+ if await self._queue_redis_log(log_entry):
323
+ return
324
+
325
+ # Fallback to HTTP logging
326
+ await self._send_http_log(log_entry)
327
+
328
+ def _transform_log_entry_to_request(self, log_entry: LogEntry) -> "LogRequest":
329
+ """
330
+ Transform LogEntry to LogRequest format for API layer.
331
+
332
+ Args:
333
+ log_entry: LogEntry to transform
334
+
335
+ Returns:
336
+ LogRequest with appropriate type and data
337
+ """
338
+ from ..api.types.logs_types import AuditLogData, GeneralLogData, LogRequest
339
+
340
+ context = log_entry.context or {}
341
+
342
+ if log_entry.level == "audit":
343
+ # Transform to AuditLogData
344
+ audit_data = AuditLogData(
345
+ entityType=context.get("entityType", context.get("resource", "unknown")),
346
+ entityId=context.get("entityId", context.get("resourceId", "unknown")),
347
+ action=context.get("action", "unknown"),
348
+ oldValues=context.get("oldValues"),
349
+ newValues=context.get("newValues"),
350
+ correlationId=log_entry.correlationId,
351
+ )
352
+ return LogRequest(type="audit", data=audit_data)
353
+ else:
354
+ # Transform to GeneralLogData
355
+ # Map level: "error" -> "error", others -> "general"
356
+ log_type = cast(
357
+ Literal["error", "general"], "error" if log_entry.level == "error" else "general"
358
+ )
359
+ general_data = GeneralLogData(
360
+ level=log_entry.level if log_entry.level != "error" else "error", # type: ignore
361
+ message=log_entry.message,
362
+ context=context,
363
+ correlationId=log_entry.correlationId,
364
+ )
365
+ return LogRequest(type=log_type, data=general_data)
366
+
358
367
  def with_context(self, context: Dict[str, Any]) -> "LoggerChain":
359
368
  """Create logger chain with context."""
360
369
  return LoggerChain(self, context, ClientLoggingOptions())
361
-
370
+
362
371
  def with_token(self, token: str) -> "LoggerChain":
363
372
  """Create logger chain with token."""
364
373
  return LoggerChain(self, {}, ClientLoggingOptions(token=token))
365
-
366
- def with_performance(self) -> "LoggerChain":
367
- """Create logger chain with performance metrics."""
368
- opts = ClientLoggingOptions()
369
- opts.performanceMetrics = True
370
- return LoggerChain(self, {}, opts)
371
-
374
+
372
375
  def without_masking(self) -> "LoggerChain":
373
376
  """Create logger chain without data masking."""
374
377
  opts = ClientLoggingOptions()
375
378
  opts.maskSensitiveData = False
376
379
  return LoggerChain(self, {}, opts)
377
380
 
381
+ def for_request(self, request: Any) -> "LoggerChain":
382
+ """
383
+ Create logger chain with request context pre-populated.
384
+
385
+ Shortcut for: logger.with_context({}).with_request(request)
378
386
 
379
- class LoggerChain:
380
- """Method chaining class for fluent logging API."""
381
-
382
- def __init__(
387
+ Args:
388
+ request: HTTP request object (FastAPI, Flask, Starlette)
389
+
390
+ Returns:
391
+ LoggerChain with request context
392
+
393
+ Example:
394
+ >>> await logger.for_request(request).info("Processing")
395
+ """
396
+ return LoggerChain(self, {}, ClientLoggingOptions()).with_request(request)
397
+
398
+ def get_log_with_request(
383
399
  self,
384
- logger: LoggerService,
400
+ request: Any,
401
+ message: str,
402
+ level: Literal["error", "audit", "info", "debug"] = "info",
385
403
  context: Optional[Dict[str, Any]] = None,
386
- options: Optional[ClientLoggingOptions] = None
387
- ):
404
+ stack_trace: Optional[str] = None,
405
+ ) -> LogEntry:
388
406
  """
389
- Initialize logger chain.
390
-
407
+ Get LogEntry object with auto-extracted request context.
408
+
409
+ Extracts IP, method, path, userAgent, correlationId, userId from request.
410
+ Returns LogEntry object ready for use in other projects' logger tables.
411
+
391
412
  Args:
392
- logger: Logger service instance
393
- context: Initial context
394
- options: Initial logging options
395
- """
396
- self.logger = logger
397
- self.context = context or {}
398
- self.options = options or ClientLoggingOptions()
399
-
400
- def add_context(self, key: str, value: Any) -> "LoggerChain":
401
- """Add context key-value pair."""
402
- self.context[key] = value
403
- return self
404
-
405
- def add_user(self, user_id: str) -> "LoggerChain":
406
- """Add user ID."""
407
- if self.options is None:
408
- self.options = ClientLoggingOptions()
409
- self.options.userId = user_id
410
- return self
411
-
412
- def add_application(self, application_id: str) -> "LoggerChain":
413
- """Add application ID."""
414
- if self.options is None:
415
- self.options = ClientLoggingOptions()
416
- self.options.applicationId = application_id
417
- return self
418
-
419
- def add_correlation(self, correlation_id: str) -> "LoggerChain":
420
- """Add correlation ID."""
421
- if self.options is None:
422
- self.options = ClientLoggingOptions()
423
- self.options.correlationId = correlation_id
424
- return self
425
-
426
- def with_token(self, token: str) -> "LoggerChain":
427
- """Add token for context extraction."""
428
- if self.options is None:
429
- self.options = ClientLoggingOptions()
430
- self.options.token = token
431
- return self
432
-
433
- def with_performance(self) -> "LoggerChain":
434
- """Enable performance metrics."""
435
- if self.options is None:
436
- self.options = ClientLoggingOptions()
437
- self.options.performanceMetrics = True
438
- return self
439
-
440
- def without_masking(self) -> "LoggerChain":
441
- """Disable data masking."""
442
- if self.options is None:
443
- self.options = ClientLoggingOptions()
444
- self.options.maskSensitiveData = False
445
- return self
446
-
447
- async def error(self, message: str, stack_trace: Optional[str] = None) -> None:
448
- """Log error."""
449
- await self.logger.error(message, self.context, stack_trace, self.options)
450
-
451
- async def info(self, message: str) -> None:
452
- """Log info."""
453
- await self.logger.info(message, self.context, self.options)
454
-
455
- async def audit(self, action: str, resource: str) -> None:
456
- """Log audit."""
457
- await self.logger.audit(action, resource, self.context, self.options)
413
+ request: HTTP request object (FastAPI, Flask, Starlette)
414
+ message: Log message
415
+ level: Log level (default: "info")
416
+ context: Additional context data (optional)
417
+ stack_trace: Stack trace for errors (optional)
418
+
419
+ Returns:
420
+ LogEntry object with request context extracted
421
+
422
+ Example:
423
+ >>> log_entry = logger.get_log_with_request(request, "Processing request")
424
+ >>> # Use log_entry in your own logger table
425
+ """
426
+ from ..utils.request_context import extract_request_context
427
+
428
+ # Extract request context
429
+ ctx = extract_request_context(request)
430
+
431
+ # Build options from extracted context
432
+ options = ClientLoggingOptions()
433
+ if ctx.user_id:
434
+ options.userId = ctx.user_id
435
+ if ctx.session_id:
436
+ options.sessionId = ctx.session_id
437
+ if ctx.correlation_id:
438
+ options.correlationId = ctx.correlation_id
439
+ if ctx.request_id:
440
+ options.requestId = ctx.request_id
441
+ if ctx.ip_address:
442
+ options.ipAddress = ctx.ip_address
443
+ if ctx.user_agent:
444
+ options.userAgent = ctx.user_agent
445
+
446
+ # Merge request info into context
447
+ request_context = context or {}
448
+ if ctx.method:
449
+ request_context["method"] = ctx.method
450
+ if ctx.path:
451
+ request_context["path"] = ctx.path
452
+ if ctx.referer:
453
+ request_context["referer"] = ctx.referer
454
+ if ctx.request_size:
455
+ request_context["requestSize"] = ctx.request_size
456
+
457
+ # Create log entry using helper function
458
+ correlation_id = (
459
+ options.correlationId if options else None
460
+ ) or self._generate_correlation_id()
461
+ return build_log_entry(
462
+ level=level,
463
+ message=message,
464
+ context=request_context,
465
+ config_client_id=self.config.client_id,
466
+ correlation_id=correlation_id,
467
+ jwt_token=options.token if options else None,
468
+ stack_trace=stack_trace,
469
+ options=options,
470
+ metadata=extract_metadata(),
471
+ mask_sensitive=self.mask_sensitive_data,
472
+ )
473
+
474
+ def get_with_context(
475
+ self,
476
+ context: Dict[str, Any],
477
+ message: str,
478
+ level: Literal["error", "audit", "info", "debug"] = "info",
479
+ stack_trace: Optional[str] = None,
480
+ options: Optional[ClientLoggingOptions] = None,
481
+ ) -> LogEntry:
482
+ """
483
+ Get LogEntry object with custom context.
484
+
485
+ Adds custom context and returns LogEntry object.
486
+ Allows projects to add their own context while leveraging MisoClient defaults.
487
+
488
+ Args:
489
+ context: Custom context data
490
+ message: Log message
491
+ level: Log level (default: "info")
492
+ stack_trace: Stack trace for errors (optional)
493
+ options: Optional logging options (optional)
494
+
495
+ Returns:
496
+ LogEntry object with custom context
497
+
498
+ Example:
499
+ >>> log_entry = logger.get_with_context(
500
+ ... {"customField": "value"},
501
+ ... "Custom log",
502
+ ... level="info"
503
+ ... )
504
+ """
505
+ final_options = options or ClientLoggingOptions()
506
+ correlation_id = (
507
+ final_options.correlationId if final_options else None
508
+ ) or self._generate_correlation_id()
509
+ return build_log_entry(
510
+ level=level,
511
+ message=message,
512
+ context=context,
513
+ config_client_id=self.config.client_id,
514
+ correlation_id=correlation_id,
515
+ jwt_token=final_options.token if final_options else None,
516
+ stack_trace=stack_trace,
517
+ options=final_options,
518
+ metadata=extract_metadata(),
519
+ mask_sensitive=self.mask_sensitive_data,
520
+ )
521
+
522
+ def get_with_token(
523
+ self,
524
+ token: str,
525
+ message: str,
526
+ level: Literal["error", "audit", "info", "debug"] = "info",
527
+ context: Optional[Dict[str, Any]] = None,
528
+ stack_trace: Optional[str] = None,
529
+ ) -> LogEntry:
530
+ """
531
+ Get LogEntry object with JWT token context extracted.
532
+
533
+ Extracts userId, sessionId from JWT token.
534
+ Returns LogEntry with user context extracted.
535
+
536
+ Args:
537
+ token: JWT token string
538
+ message: Log message
539
+ level: Log level (default: "info")
540
+ context: Additional context data (optional)
541
+ stack_trace: Stack trace for errors (optional)
542
+
543
+ Returns:
544
+ LogEntry object with user context extracted
545
+
546
+ Example:
547
+ >>> log_entry = logger.get_with_token(
548
+ ... "jwt-token",
549
+ ... "User action",
550
+ ... level="audit"
551
+ ... )
552
+ """
553
+ options = ClientLoggingOptions(token=token)
554
+ correlation_id = (
555
+ options.correlationId if options else None
556
+ ) or self._generate_correlation_id()
557
+ return build_log_entry(
558
+ level=level,
559
+ message=message,
560
+ context=context,
561
+ config_client_id=self.config.client_id,
562
+ correlation_id=correlation_id,
563
+ jwt_token=token,
564
+ stack_trace=stack_trace,
565
+ options=options,
566
+ metadata=extract_metadata(),
567
+ mask_sensitive=self.mask_sensitive_data,
568
+ )
569
+
570
+ def get_for_request(
571
+ self,
572
+ request: Any,
573
+ message: str,
574
+ level: Literal["error", "audit", "info", "debug"] = "info",
575
+ context: Optional[Dict[str, Any]] = None,
576
+ stack_trace: Optional[str] = None,
577
+ ) -> LogEntry:
578
+ """
579
+ Get LogEntry object with request context (alias for get_log_with_request).
580
+
581
+ Same functionality as get_log_with_request() for convenience.
582
+
583
+ Args:
584
+ request: HTTP request object (FastAPI, Flask, Starlette)
585
+ message: Log message
586
+ level: Log level (default: "info")
587
+ context: Additional context data (optional)
588
+ stack_trace: Stack trace for errors (optional)
589
+
590
+ Returns:
591
+ LogEntry object with request context extracted
592
+
593
+ Example:
594
+ >>> log_entry = logger.get_for_request(request, "Request processed")
595
+ """
596
+ return self.get_log_with_request(request, message, level, context, stack_trace)