elven-logs-interceptor-python 0.1.16__tar.gz → 0.1.18__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.16 → elven_logs_interceptor_python-0.1.18}/PKG-INFO +23 -10
  2. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/README.md +19 -6
  3. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/pyproject.toml +3 -3
  4. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/application/config_service.py +12 -0
  5. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/utils.py +104 -13
  6. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/.gitignore +0 -0
  7. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/ARCHITECTURE.md +0 -0
  8. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/__init__.py +0 -0
  9. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/application/__init__.py +0 -0
  10. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/application/log_service.py +0 -0
  11. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/config.py +0 -0
  12. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/domain/__init__.py +0 -0
  13. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/domain/entities.py +0 -0
  14. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/domain/interfaces.py +0 -0
  15. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/domain/value_objects.py +0 -0
  16. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/__init__.py +0 -0
  17. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/buffer/__init__.py +0 -0
  18. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/buffer/memory_buffer.py +0 -0
  19. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/circuit_breaker/__init__.py +0 -0
  20. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/circuit_breaker/circuit_breaker.py +0 -0
  21. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/compression/__init__.py +0 -0
  22. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/compression/base.py +0 -0
  23. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/compression/brotli_compressor.py +0 -0
  24. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/compression/factory.py +0 -0
  25. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/compression/gzip_compressor.py +0 -0
  26. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/compression/noop_compressor.py +0 -0
  27. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/context/__init__.py +0 -0
  28. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/context/context_provider.py +0 -0
  29. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/dlq/__init__.py +0 -0
  30. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/dlq/file_dlq.py +0 -0
  31. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/dlq/memory_dlq.py +0 -0
  32. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/filter/__init__.py +0 -0
  33. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/filter/log_filter.py +0 -0
  34. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/interceptors/__init__.py +0 -0
  35. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/interceptors/runtime_interceptor.py +0 -0
  36. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/internal_capture_guard.py +0 -0
  37. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/log_noise_filter.py +0 -0
  38. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/log_record_extra.py +0 -0
  39. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/memory/__init__.py +0 -0
  40. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/memory/memory_tracker.py +0 -0
  41. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/metrics/__init__.py +0 -0
  42. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/metrics/metrics_collector.py +0 -0
  43. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/transport/__init__.py +0 -0
  44. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/transport/loki_json_transport.py +0 -0
  45. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/transport/loki_protobuf_transport.py +0 -0
  46. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/transport/otlp_json_transport.py +0 -0
  47. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/transport/resilient_transport.py +0 -0
  48. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/transport/transport_factory.py +0 -0
  49. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/workers/__init__.py +0 -0
  50. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/infrastructure/workers/worker_pool.py +0 -0
  51. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/integrations/__init__.py +0 -0
  52. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/integrations/celery.py +0 -0
  53. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/integrations/django.py +0 -0
  54. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/integrations/fastapi.py +0 -0
  55. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/integrations/flask.py +0 -0
  56. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/integrations/logging_handler.py +0 -0
  57. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/integrations/loguru.py +0 -0
  58. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/integrations/structlog.py +0 -0
  59. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/preload.py +0 -0
  60. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/presentation/__init__.py +0 -0
  61. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/presentation/factory.py +0 -0
  62. {elven_logs_interceptor_python-0.1.16 → elven_logs_interceptor_python-0.1.18}/src/logs_interceptor/types.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: elven-logs-interceptor-python
3
- Version: 0.1.16
3
+ Version: 0.1.18
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
@@ -23,7 +23,7 @@ Requires-Dist: django>=4.2; extra == 'all'
23
23
  Requires-Dist: fastapi>=0.100.0; extra == 'all'
24
24
  Requires-Dist: flask>=3.0.0; extra == 'all'
25
25
  Requires-Dist: loguru>=0.7.2; extra == 'all'
26
- Requires-Dist: opentelemetry-api>=1.24.0; extra == 'all'
26
+ Requires-Dist: opentelemetry-api<2.0.0,>=1.45.1; extra == 'all'
27
27
  Requires-Dist: orjson>=3.10.0; extra == 'all'
28
28
  Requires-Dist: protobuf>=5.0.0; extra == 'all'
29
29
  Requires-Dist: python-snappy>=0.7.1; extra == 'all'
@@ -49,7 +49,7 @@ Requires-Dist: flask>=3.0.0; extra == 'flask'
49
49
  Provides-Extra: loguru
50
50
  Requires-Dist: loguru>=0.7.2; extra == 'loguru'
51
51
  Provides-Extra: otel
52
- Requires-Dist: opentelemetry-api>=1.24.0; extra == 'otel'
52
+ Requires-Dist: opentelemetry-api<2.0.0,>=1.45.1; extra == 'otel'
53
53
  Provides-Extra: perf
54
54
  Requires-Dist: brotli>=1.1.0; extra == 'perf'
55
55
  Requires-Dist: orjson>=3.10.0; extra == 'perf'
@@ -75,6 +75,15 @@ With all extras:
75
75
  pip install "elven-logs-interceptor-python[all]"
76
76
  ```
77
77
 
78
+ ### OpenTelemetry Security
79
+
80
+ Version 0.1.18 raises the optional `otel` and `all` extras to
81
+ `opentelemetry-api>=1.45.1,<2.0.0`, excluding the vulnerable parser described in
82
+ [CVE-2026-114603](https://github.com/open-telemetry/opentelemetry-python/security/advisories/GHSA-xcq7-p3pf-p88h).
83
+ The base installation still does not require OpenTelemetry. Logging APIs and
84
+ framework minimum versions are unchanged. Existing application lockfiles must
85
+ be regenerated to receive the security update.
86
+
78
87
  ## Quick Start
79
88
 
80
89
  ```python
@@ -123,12 +132,16 @@ logging.getLogger(__name__).info(
123
132
 
124
133
  Supported aliases: `warning(...)`, `exception(...)` and `critical(...)`.
125
134
 
126
- Sensitive fields such as `cpf`, `cnpj`, `cpfCnpj`, `password`, `token` and `authorization` are
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.
135
+ Sensitive fields such as `cpf`, `cnpj`, `cpfCnpj`, `documento`,
136
+ `numeroDocumento`, `taxId`, `apiKey`, `clientSecret`, `senha`, `password`,
137
+ `token`, `authorization`, `cartao/cartão`, `chavePix` and related credential
138
+ fields are redacted by default. In messages, the library redacts only the
139
+ sensitive fragment/value and preserves the rest of the log. In structured
140
+ context, sensitive keys redact the full value. The key matcher normalizes
141
+ common `snake_case`, `camelCase`, `kebab-case`, dotted variants, and accents.
142
+ Set `LOGS_FILTER_SANITIZE=false`, `LOGS_SANITIZE=false`, or
143
+ `UO_LOGS_SANITIZE=false` only when raw sensitive data is explicitly required
144
+ and approved.
132
145
 
133
146
  ## Environment Variables
134
147
 
@@ -14,6 +14,15 @@ With all extras:
14
14
  pip install "elven-logs-interceptor-python[all]"
15
15
  ```
16
16
 
17
+ ### OpenTelemetry Security
18
+
19
+ Version 0.1.18 raises the optional `otel` and `all` extras to
20
+ `opentelemetry-api>=1.45.1,<2.0.0`, excluding the vulnerable parser described in
21
+ [CVE-2026-114603](https://github.com/open-telemetry/opentelemetry-python/security/advisories/GHSA-xcq7-p3pf-p88h).
22
+ The base installation still does not require OpenTelemetry. Logging APIs and
23
+ framework minimum versions are unchanged. Existing application lockfiles must
24
+ be regenerated to receive the security update.
25
+
17
26
  ## Quick Start
18
27
 
19
28
  ```python
@@ -62,12 +71,16 @@ logging.getLogger(__name__).info(
62
71
 
63
72
  Supported aliases: `warning(...)`, `exception(...)` and `critical(...)`.
64
73
 
65
- Sensitive fields such as `cpf`, `cnpj`, `cpfCnpj`, `password`, `token` and `authorization` are
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.
74
+ Sensitive fields such as `cpf`, `cnpj`, `cpfCnpj`, `documento`,
75
+ `numeroDocumento`, `taxId`, `apiKey`, `clientSecret`, `senha`, `password`,
76
+ `token`, `authorization`, `cartao/cartão`, `chavePix` and related credential
77
+ fields are redacted by default. In messages, the library redacts only the
78
+ sensitive fragment/value and preserves the rest of the log. In structured
79
+ context, sensitive keys redact the full value. The key matcher normalizes
80
+ common `snake_case`, `camelCase`, `kebab-case`, dotted variants, and accents.
81
+ Set `LOGS_FILTER_SANITIZE=false`, `LOGS_SANITIZE=false`, or
82
+ `UO_LOGS_SANITIZE=false` only when raw sensitive data is explicitly required
83
+ and approved.
71
84
 
72
85
  ## Environment Variables
73
86
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "elven-logs-interceptor-python"
7
- version = "0.1.16"
7
+ version = "0.1.18"
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"
@@ -44,7 +44,7 @@ perf = [
44
44
  "brotli>=1.1.0",
45
45
  ]
46
46
  otel = [
47
- "opentelemetry-api>=1.24.0",
47
+ "opentelemetry-api>=1.45.1,<2.0.0",
48
48
  ]
49
49
  fastapi = [
50
50
  "fastapi>=0.100.0",
@@ -70,7 +70,7 @@ all = [
70
70
  "protobuf>=5.0.0",
71
71
  "python-snappy>=0.7.1",
72
72
  "brotli>=1.1.0",
73
- "opentelemetry-api>=1.24.0",
73
+ "opentelemetry-api>=1.45.1,<2.0.0",
74
74
  "fastapi>=0.100.0",
75
75
  "starlette>=0.27.0",
76
76
  "django>=4.2",
@@ -24,13 +24,25 @@ from ..types import LogLevel
24
24
  DEFAULT_LEVELS: list[LogLevel] = ["debug", "info", "warn", "error", "fatal"]
25
25
  DEFAULT_SENSITIVE_PATTERNS = [
26
26
  r"password",
27
+ r"senha",
27
28
  r"token",
28
29
  r"secret",
30
+ r"segredo",
29
31
  r"api[_-]?key",
32
+ r"chave[_-]?api",
30
33
  r"authorization",
34
+ r"credential",
31
35
  r"credit[_-]?card",
36
+ r"cartao",
32
37
  r"ssn",
33
38
  r"cpf[_-]?cnpj",
39
+ r"cnpj[_-]?cpf",
40
+ r"tax[_-]?id",
41
+ r"document[_-]?number",
42
+ r"documento",
43
+ r"numero[_-]?documento",
44
+ r"passaporte",
45
+ r"passport",
34
46
  r"cnpj",
35
47
  r"cpf",
36
48
  ]
@@ -8,6 +8,7 @@ import re
8
8
  import secrets
9
9
  import sys
10
10
  import traceback
11
+ import unicodedata
11
12
  from collections.abc import Iterable, Mapping
12
13
  from dataclasses import asdict, fields, is_dataclass
13
14
  from datetime import datetime
@@ -39,6 +40,58 @@ except Exception: # pragma: no cover - optional dependency
39
40
  TRUE_VALUES = {"1", "true", "yes", "on"}
40
41
  FALSE_VALUES = {"0", "false", "no", "off"}
41
42
 
43
+ _SENSITIVE_KEY_TOKENS = (
44
+ "password",
45
+ "passwd",
46
+ "passphrase",
47
+ "pwd",
48
+ "senha",
49
+ "senhas",
50
+ "token",
51
+ "jwt",
52
+ "secret",
53
+ "segredo",
54
+ "clientsecret",
55
+ "privatekey",
56
+ "apikey",
57
+ "chaveapi",
58
+ "chavesecreta",
59
+ "chaveacesso",
60
+ "chavepix",
61
+ "accesskey",
62
+ "authorization",
63
+ "credentials",
64
+ "credential",
65
+ "credenciais",
66
+ "credencial",
67
+ "cpfcnpj",
68
+ "cnpjcpf",
69
+ "cpf",
70
+ "cnpj",
71
+ "taxid",
72
+ "taxidentifier",
73
+ "documentnumber",
74
+ "documentid",
75
+ "documento",
76
+ "numerodocumento",
77
+ "documentoidentificacao",
78
+ "passport",
79
+ "passaporte",
80
+ "ssn",
81
+ "creditcard",
82
+ "cardnumber",
83
+ "cartao",
84
+ "numerocartao",
85
+ "cvv",
86
+ "cvc",
87
+ "pixkey",
88
+ "bankaccount",
89
+ "accountnumber",
90
+ "numeroconta",
91
+ "iban",
92
+ "email",
93
+ )
94
+
42
95
 
43
96
  def parse_bool(value: str | None, default: bool) -> bool:
44
97
  if value is None:
@@ -395,10 +448,7 @@ def detect_sensitive_data(text: str, patterns: Iterable[str]) -> bool:
395
448
  def redact_sensitive_text(text: str, sensitive_patterns: Iterable[str]) -> str:
396
449
  """Redact sensitive values while preserving the surrounding log message."""
397
450
  patterns = list(sensitive_patterns)
398
- redacted = text
399
-
400
- for pattern in patterns:
401
- redacted = _redact_key_value_pattern(redacted, pattern)
451
+ redacted = _redact_sensitive_key_values(text, patterns)
402
452
 
403
453
  common_replacements: tuple[tuple[re.Pattern[str], str], ...] = (
404
454
  (
@@ -436,31 +486,70 @@ def redact_sensitive_text(text: str, sensitive_patterns: Iterable[str]) -> str:
436
486
  return redacted
437
487
 
438
488
 
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)",
489
+ def _redact_sensitive_key_values(text: str, sensitive_patterns: Iterable[str]) -> str:
490
+ key_value = re.compile(
491
+ r"(?P<key_quote>[\"']?)(?P<key>[A-Za-z_][\w.\-]*)(?P=key_quote)"
492
+ r"(?P<separator>\s*[:=]\s*)"
493
+ r"(?P<value_quote>[\"'])(?P<value>.*?)(?P=value_quote)",
442
494
  re.IGNORECASE,
443
495
  )
444
- redacted = quoted.sub(r"\g<prefix>\g<quote>[REDACTED]\g<quote>", text)
496
+
497
+ def _replace_key_value(match: re.Match[str]) -> str:
498
+ if not _is_sensitive_key(match.group("key"), sensitive_patterns):
499
+ return match.group(0)
500
+ return (
501
+ f"{match.group('key_quote')}{match.group('key')}{match.group('key_quote')}"
502
+ f"{match.group('separator')}{match.group('value_quote')}[REDACTED]"
503
+ f"{match.group('value_quote')}"
504
+ )
505
+
506
+ redacted = key_value.sub(_replace_key_value, text)
445
507
 
446
508
  unquoted = re.compile(
447
- rf"(?P<prefix>(?:[\"']?{key_pattern}[\"']?)\s*[:=]\s*)(?P<value>[^\s,;&}}\]\"']+)",
509
+ r"(?P<key>[A-Za-z_][\w.\-]*)"
510
+ r"(?P<separator>\s*[:=]\s*)"
511
+ r"(?P<value>[^\s,;&}}\]\"']+)",
448
512
  re.IGNORECASE,
449
513
  )
450
514
 
451
515
  def _replace_unquoted(match: re.Match[str]) -> str:
516
+ if not _is_sensitive_key(match.group("key"), sensitive_patterns):
517
+ return match.group(0)
452
518
  value = match.group("value")
453
519
  if value.lower() in {"bearer", "basic"} or value == "[REDACTED]":
454
520
  return match.group(0)
455
- return f"{match.group('prefix')}[REDACTED]"
521
+ return f"{match.group('key')}{match.group('separator')}[REDACTED]"
456
522
 
457
523
  redacted = unquoted.sub(_replace_unquoted, redacted)
458
524
 
459
525
  query = re.compile(
460
- rf"(?P<prefix>[?&](?:{key_pattern})=)(?P<value>[^&#\s]+)",
526
+ r"(?P<prefix>[?&](?P<key>[A-Za-z_][\w.\-]*)=)(?P<value>[^&#\s]+)",
461
527
  re.IGNORECASE,
462
528
  )
463
- return query.sub(r"\g<prefix>[REDACTED]", redacted)
529
+
530
+ def _replace_query(match: re.Match[str]) -> str:
531
+ if not _is_sensitive_key(match.group("key"), sensitive_patterns):
532
+ return match.group(0)
533
+ return f"{match.group('prefix')}[REDACTED]"
534
+
535
+ return query.sub(_replace_query, redacted)
536
+
537
+
538
+ def _is_sensitive_key(key: str, sensitive_patterns: Iterable[str]) -> bool:
539
+ compiled = [re.compile(pattern, re.IGNORECASE) for pattern in sensitive_patterns]
540
+ if any(pattern.search(key) for pattern in compiled):
541
+ return True
542
+ normalized = _normalize_sensitive_key(key)
543
+ return any(token in normalized for token in _SENSITIVE_KEY_TOKENS)
544
+
545
+
546
+ def _normalize_sensitive_key(key: str) -> str:
547
+ ascii_key = (
548
+ unicodedata.normalize("NFKD", key)
549
+ .encode("ascii", "ignore")
550
+ .decode("ascii")
551
+ )
552
+ return re.sub(r"[^a-z0-9]", "", ascii_key.lower())
464
553
 
465
554
 
466
555
  def _looks_like_value_pattern(pattern: str) -> bool:
@@ -490,7 +579,9 @@ def sanitize_data(
490
579
  sanitized: dict[str, Any] = {}
491
580
 
492
581
  for key, value in data.items():
493
- key_sensitive = any(pattern.search(key) for pattern in compiled)
582
+ key_sensitive = any(pattern.search(key) for pattern in compiled) or _is_sensitive_key(
583
+ key, sensitive_patterns
584
+ )
494
585
  if key_sensitive:
495
586
  sanitized[key] = "[REDACTED]"
496
587
  continue