elven-logs-interceptor-python 0.1.13__tar.gz → 0.1.15__tar.gz

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. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/PKG-INFO +14 -8
  2. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/README.md +13 -7
  3. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/pyproject.toml +1 -1
  4. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/filter/log_filter.py +3 -3
  5. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/utils.py +124 -9
  6. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/.gitignore +0 -0
  7. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/ARCHITECTURE.md +0 -0
  8. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/__init__.py +0 -0
  9. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/application/__init__.py +0 -0
  10. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/application/config_service.py +0 -0
  11. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/application/log_service.py +0 -0
  12. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/config.py +0 -0
  13. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/domain/__init__.py +0 -0
  14. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/domain/entities.py +0 -0
  15. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/domain/interfaces.py +0 -0
  16. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/domain/value_objects.py +0 -0
  17. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/__init__.py +0 -0
  18. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/buffer/__init__.py +0 -0
  19. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/buffer/memory_buffer.py +0 -0
  20. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/circuit_breaker/__init__.py +0 -0
  21. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/circuit_breaker/circuit_breaker.py +0 -0
  22. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/compression/__init__.py +0 -0
  23. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/compression/base.py +0 -0
  24. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/compression/brotli_compressor.py +0 -0
  25. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/compression/factory.py +0 -0
  26. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/compression/gzip_compressor.py +0 -0
  27. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/compression/noop_compressor.py +0 -0
  28. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/context/__init__.py +0 -0
  29. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/context/context_provider.py +0 -0
  30. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/dlq/__init__.py +0 -0
  31. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/dlq/file_dlq.py +0 -0
  32. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/dlq/memory_dlq.py +0 -0
  33. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/filter/__init__.py +0 -0
  34. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/interceptors/__init__.py +0 -0
  35. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +0 -0
  36. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/internal_capture_guard.py +0 -0
  37. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/log_noise_filter.py +0 -0
  38. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/log_record_extra.py +0 -0
  39. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/memory/__init__.py +0 -0
  40. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/memory/memory_tracker.py +0 -0
  41. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/metrics/__init__.py +0 -0
  42. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/metrics/metrics_collector.py +0 -0
  43. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/transport/__init__.py +0 -0
  44. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/transport/loki_json_transport.py +0 -0
  45. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/transport/loki_protobuf_transport.py +0 -0
  46. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/transport/otlp_json_transport.py +0 -0
  47. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/transport/resilient_transport.py +0 -0
  48. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/transport/transport_factory.py +0 -0
  49. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/workers/__init__.py +0 -0
  50. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/infrastructure/workers/worker_pool.py +0 -0
  51. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/integrations/__init__.py +0 -0
  52. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/integrations/celery.py +0 -0
  53. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/integrations/django.py +0 -0
  54. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/integrations/fastapi.py +0 -0
  55. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/integrations/flask.py +0 -0
  56. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/integrations/logging_handler.py +0 -0
  57. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/integrations/loguru.py +0 -0
  58. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/integrations/structlog.py +0 -0
  59. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/preload.py +0 -0
  60. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/presentation/__init__.py +0 -0
  61. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/presentation/factory.py +0 -0
  62. {elven_logs_interceptor_python-0.1.13 → elven_logs_interceptor_python-0.1.15}/src/logs_interceptor/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elven-logs-interceptor-python
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations.
5
5
  Author: Elven Observability
6
6
  License: MIT
@@ -124,8 +124,11 @@ logging.getLogger(__name__).info(
124
124
  Supported aliases: `warning(...)`, `exception(...)` and `critical(...)`.
125
125
 
126
126
  Sensitive fields such as `cpf`, `password`, `token` and `authorization` are
127
- redacted by default. Set `LOGS_FILTER_SANITIZE=false` only when raw sensitive
128
- data is explicitly required and approved.
127
+ redacted by default. In messages, the library redacts only the sensitive
128
+ fragment/value and preserves the rest of the log. In structured context,
129
+ sensitive keys redact the full value. Set `LOGS_FILTER_SANITIZE=false`,
130
+ `LOGS_SANITIZE=false`, or `UO_LOGS_SANITIZE=false` only when raw sensitive data
131
+ is explicitly required and approved.
129
132
 
130
133
  ## Environment Variables
131
134
 
@@ -139,10 +142,13 @@ Required for direct Loki mode:
139
142
  - `LOGS_APP_NAME`
140
143
 
141
144
  For collector-first mode, set `LOGS_EXPORTER=otlp` (or `collector`) and use
142
- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`. In this mode `LOGS_URL`, `LOKI_URL`,
143
- `LOGS_TENANT` and `LOGS_TOKEN` are not required by the library. `LOGS_URL` is
144
- intentionally ignored in collector mode to avoid accidentally posting OTLP logs
145
- to a direct Loki `/loki/api/v1/push` endpoint. Put tenant or auth headers in
145
+ `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`. If the explicit logs endpoint is missing,
146
+ the library derives `/v1/logs` from `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,
147
+ `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, or `OTEL_EXPORTER_OTLP_ENDPOINT`.
148
+ In this mode `LOGS_URL`, `LOKI_URL`, `LOGS_TENANT` and `LOGS_TOKEN` are not
149
+ required by the library. `LOGS_URL` is intentionally ignored in collector mode
150
+ to avoid accidentally posting OTLP logs to a direct Loki `/loki/api/v1/push`
151
+ endpoint. Put tenant or auth headers in
146
152
  `OTEL_EXPORTER_OTLP_HEADERS`/`LOGS_OTLP_HEADERS` if your collector gateway
147
153
  requires them.
148
154
 
@@ -182,7 +188,7 @@ Filter:
182
188
 
183
189
  - `LOGS_FILTER_LEVELS`
184
190
  - `LOGS_FILTER_SAMPLING_RATE`
185
- - `LOGS_FILTER_SANITIZE`
191
+ - `LOGS_FILTER_SANITIZE` (aliases: `LOGS_SANITIZE`, `UO_LOGS_SANITIZE`)
186
192
  - `LOGS_FILTER_MAX_MESSAGE_LENGTH`
187
193
  - `LOGS_FILTER_EXCLUDE_LOGGER_PREFIXES` (comma-separated)
188
194
 
@@ -63,8 +63,11 @@ logging.getLogger(__name__).info(
63
63
  Supported aliases: `warning(...)`, `exception(...)` and `critical(...)`.
64
64
 
65
65
  Sensitive fields such as `cpf`, `password`, `token` and `authorization` are
66
- redacted by default. Set `LOGS_FILTER_SANITIZE=false` only when raw sensitive
67
- data is explicitly required and approved.
66
+ redacted by default. In messages, the library redacts only the sensitive
67
+ fragment/value and preserves the rest of the log. In structured context,
68
+ sensitive keys redact the full value. Set `LOGS_FILTER_SANITIZE=false`,
69
+ `LOGS_SANITIZE=false`, or `UO_LOGS_SANITIZE=false` only when raw sensitive data
70
+ is explicitly required and approved.
68
71
 
69
72
  ## Environment Variables
70
73
 
@@ -78,10 +81,13 @@ Required for direct Loki mode:
78
81
  - `LOGS_APP_NAME`
79
82
 
80
83
  For collector-first mode, set `LOGS_EXPORTER=otlp` (or `collector`) and use
81
- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`. In this mode `LOGS_URL`, `LOKI_URL`,
82
- `LOGS_TENANT` and `LOGS_TOKEN` are not required by the library. `LOGS_URL` is
83
- intentionally ignored in collector mode to avoid accidentally posting OTLP logs
84
- to a direct Loki `/loki/api/v1/push` endpoint. Put tenant or auth headers in
84
+ `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`. If the explicit logs endpoint is missing,
85
+ the library derives `/v1/logs` from `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,
86
+ `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, or `OTEL_EXPORTER_OTLP_ENDPOINT`.
87
+ In this mode `LOGS_URL`, `LOKI_URL`, `LOGS_TENANT` and `LOGS_TOKEN` are not
88
+ required by the library. `LOGS_URL` is intentionally ignored in collector mode
89
+ to avoid accidentally posting OTLP logs to a direct Loki `/loki/api/v1/push`
90
+ endpoint. Put tenant or auth headers in
85
91
  `OTEL_EXPORTER_OTLP_HEADERS`/`LOGS_OTLP_HEADERS` if your collector gateway
86
92
  requires them.
87
93
 
@@ -121,7 +127,7 @@ Filter:
121
127
 
122
128
  - `LOGS_FILTER_LEVELS`
123
129
  - `LOGS_FILTER_SAMPLING_RATE`
124
- - `LOGS_FILTER_SANITIZE`
130
+ - `LOGS_FILTER_SANITIZE` (aliases: `LOGS_SANITIZE`, `UO_LOGS_SANITIZE`)
125
131
  - `LOGS_FILTER_MAX_MESSAGE_LENGTH`
126
132
  - `LOGS_FILTER_EXCLUDE_LOGGER_PREFIXES` (comma-separated)
127
133
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "elven-logs-interceptor-python"
7
- version = "0.1.13"
7
+ version = "0.1.15"
8
8
  description = "Production-grade logs interceptor for Python with Loki transport, resilience, batching, and framework integrations."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -5,7 +5,7 @@ import re
5
5
  from ...config import ResolvedFilterConfig
6
6
  from ...domain.entities import LogEntryEntity
7
7
  from ...types import LogLevel
8
- from ...utils import detect_sensitive_data, sanitize_data, should_sample
8
+ from ...utils import redact_sensitive_text, sanitize_data, should_sample
9
9
 
10
10
 
11
11
  class LogFilter:
@@ -35,8 +35,8 @@ class LogFilter:
35
35
  if self._config.sanitize and context is not None:
36
36
  context = sanitize_data(context, self._config.sensitive_patterns)
37
37
 
38
- if self._config.sanitize and detect_sensitive_data(message, self._config.sensitive_patterns):
39
- message = "[REDACTED]"
38
+ if self._config.sanitize:
39
+ message = redact_sensitive_text(message, self._config.sensitive_patterns)
40
40
 
41
41
  return LogEntryEntity(
42
42
  id=entry.id,
@@ -82,6 +82,17 @@ def _normalize_logs_exporter(value: str | None) -> str:
82
82
  return "loki"
83
83
 
84
84
 
85
+ def _resolve_logs_exporter(env: Mapping[str, str]) -> str:
86
+ explicit = env.get("LOGS_EXPORTER") or env.get("LOGS_TRANSPORT") or env.get("OTEL_LOGS_EXPORTER")
87
+ if explicit:
88
+ return _normalize_logs_exporter(explicit)
89
+ if env.get("LOGS_URL"):
90
+ return "loki"
91
+ if _resolve_otlp_logs_url(env):
92
+ return "otlp"
93
+ return "loki"
94
+
95
+
85
96
  def _parse_kv_csv(raw: str | None) -> dict[str, str]:
86
97
  headers: dict[str, str] = {}
87
98
  if not raw:
@@ -99,7 +110,29 @@ def _parse_kv_csv(raw: str | None) -> dict[str, str]:
99
110
  def _resolve_logs_url(env: Mapping[str, str], exporter: str) -> str:
100
111
  if exporter != "otlp":
101
112
  return env.get("LOGS_URL", "")
102
- return env.get("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "")
113
+ return _resolve_otlp_logs_url(env)
114
+
115
+
116
+ def _resolve_otlp_logs_url(env: Mapping[str, str]) -> str:
117
+ return (
118
+ env.get("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT")
119
+ or _derive_otlp_logs_endpoint(
120
+ env.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
121
+ or env.get("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT")
122
+ or env.get("OTEL_EXPORTER_OTLP_ENDPOINT")
123
+ )
124
+ )
125
+
126
+
127
+ def _derive_otlp_logs_endpoint(endpoint: str | None) -> str:
128
+ if not endpoint:
129
+ return ""
130
+ normalized = endpoint.rstrip("/")
131
+ if normalized.endswith("/v1/logs"):
132
+ return normalized
133
+ if normalized.endswith("/v1/traces") or normalized.endswith("/v1/metrics"):
134
+ return normalized.rsplit("/", 1)[0] + "/logs"
135
+ return normalized + "/v1/logs"
103
136
 
104
137
 
105
138
  def is_debug_enabled() -> bool:
@@ -359,6 +392,87 @@ def detect_sensitive_data(text: str, patterns: Iterable[str]) -> bool:
359
392
  return any(pattern.search(text) for pattern in common_patterns)
360
393
 
361
394
 
395
+ def redact_sensitive_text(text: str, sensitive_patterns: Iterable[str]) -> str:
396
+ """Redact sensitive values while preserving the surrounding log message."""
397
+ patterns = list(sensitive_patterns)
398
+ redacted = text
399
+
400
+ for pattern in patterns:
401
+ redacted = _redact_key_value_pattern(redacted, pattern)
402
+
403
+ common_replacements: tuple[tuple[re.Pattern[str], str], ...] = (
404
+ (
405
+ re.compile(r"(Bearer)\s+[A-Za-z0-9\-._~+/=]+", re.IGNORECASE),
406
+ r"\1 [REDACTED]",
407
+ ),
408
+ (
409
+ re.compile(r"(Basic)\s+[A-Za-z0-9+/=]+", re.IGNORECASE),
410
+ r"\1 [REDACTED]",
411
+ ),
412
+ (
413
+ re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"),
414
+ "[REDACTED]",
415
+ ),
416
+ (
417
+ re.compile(r"\b(?:\d{4}[-\s]?){3}\d{4}\b"),
418
+ "[REDACTED]",
419
+ ),
420
+ (
421
+ re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
422
+ "[REDACTED]",
423
+ ),
424
+ (
425
+ re.compile(r"\b\d{3}\.\d{3}\.\d{3}-\d{2}\b"),
426
+ "[REDACTED]",
427
+ ),
428
+ )
429
+ for common_pattern, replacement in common_replacements:
430
+ redacted = common_pattern.sub(replacement, redacted)
431
+
432
+ for pattern in patterns:
433
+ if _looks_like_value_pattern(pattern):
434
+ redacted = re.sub(pattern, "[REDACTED]", redacted, flags=re.IGNORECASE)
435
+
436
+ return redacted
437
+
438
+
439
+ def _redact_key_value_pattern(text: str, key_pattern: str) -> str:
440
+ quoted = re.compile(
441
+ rf"(?P<prefix>(?:[\"']?{key_pattern}[\"']?)\s*[:=]\s*)(?P<quote>[\"'])(?P<value>.*?)(?P=quote)",
442
+ re.IGNORECASE,
443
+ )
444
+ redacted = quoted.sub(r"\g<prefix>\g<quote>[REDACTED]\g<quote>", text)
445
+
446
+ unquoted = re.compile(
447
+ rf"(?P<prefix>(?:[\"']?{key_pattern}[\"']?)\s*[:=]\s*)(?P<value>[^\s,;&}}\]\"']+)",
448
+ re.IGNORECASE,
449
+ )
450
+
451
+ def _replace_unquoted(match: re.Match[str]) -> str:
452
+ value = match.group("value")
453
+ if value.lower() in {"bearer", "basic"} or value == "[REDACTED]":
454
+ return match.group(0)
455
+ return f"{match.group('prefix')}[REDACTED]"
456
+
457
+ redacted = unquoted.sub(_replace_unquoted, redacted)
458
+
459
+ query = re.compile(
460
+ rf"(?P<prefix>[?&](?:{key_pattern})=)(?P<value>[^&#\s]+)",
461
+ re.IGNORECASE,
462
+ )
463
+ return query.sub(r"\g<prefix>[REDACTED]", redacted)
464
+
465
+
466
+ def _looks_like_value_pattern(pattern: str) -> bool:
467
+ # Plain words such as "token" and "password" are key indicators by default;
468
+ # redacting those words directly would destroy harmless messages like
469
+ # "Gerando token de acesso". Regex-looking patterns are treated as values.
470
+ keyish_terms = r"password|token|secret|api|key|authorization|credit|card|ssn|cpf"
471
+ if re.search(keyish_terms, pattern, re.IGNORECASE):
472
+ return False
473
+ return bool(re.search(r"\\d|\[0-9|\{\d|[\^$]", pattern))
474
+
475
+
362
476
  def sanitize_data(
363
477
  data: dict[str, Any],
364
478
  sensitive_patterns: Iterable[str],
@@ -382,16 +496,14 @@ def sanitize_data(
382
496
  continue
383
497
 
384
498
  if isinstance(value, str):
385
- sanitized[key] = "[REDACTED]" if detect_sensitive_data(value, sensitive_patterns) else value
499
+ sanitized[key] = redact_sensitive_text(value, sensitive_patterns)
386
500
  continue
387
501
 
388
502
  if isinstance(value, list):
389
503
  transformed: list[Any] = []
390
504
  for item in value:
391
505
  if isinstance(item, str):
392
- transformed.append(
393
- "[REDACTED]" if detect_sensitive_data(item, sensitive_patterns) else item
394
- )
506
+ transformed.append(redact_sensitive_text(item, sensitive_patterns))
395
507
  elif isinstance(item, dict):
396
508
  transformed.append(sanitize_data(item, sensitive_patterns, seen))
397
509
  else:
@@ -534,9 +646,7 @@ def load_config_from_env() -> LogsInterceptorConfig:
534
646
  if compression_value not in {"none", "gzip", "brotli", "snappy"}:
535
647
  compression_value = "gzip"
536
648
 
537
- exporter = _normalize_logs_exporter(
538
- env.get("LOGS_EXPORTER") or env.get("LOGS_TRANSPORT") or env.get("OTEL_LOGS_EXPORTER")
539
- )
649
+ exporter = _resolve_logs_exporter(env)
540
650
  headers = {
541
651
  **_parse_kv_csv(env.get("LOGS_HEADERS")),
542
652
  **_parse_kv_csv(env.get("LOGS_OTLP_HEADERS")),
@@ -588,7 +698,12 @@ def load_config_from_env() -> LogsInterceptorConfig:
588
698
  filter=FilterConfig(
589
699
  levels=_env_levels(env.get("LOGS_FILTER_LEVELS")),
590
700
  sampling_rate=parse_float_range(env.get("LOGS_FILTER_SAMPLING_RATE"), 1.0, 0.0, 1.0),
591
- sanitize=parse_bool(env.get("LOGS_FILTER_SANITIZE"), True),
701
+ sanitize=parse_bool(
702
+ env.get("LOGS_FILTER_SANITIZE")
703
+ or env.get("LOGS_SANITIZE")
704
+ or env.get("UO_LOGS_SANITIZE"),
705
+ True,
706
+ ),
592
707
  max_message_length=parse_int_range(
593
708
  env.get("LOGS_FILTER_MAX_MESSAGE_LENGTH"), 8192, 64, 1_000_000
594
709
  ),