Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 1 addition & 17 deletions .secrets.baseline

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 8 additions & 49 deletions src/cachekit/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ def _get_logging_config():

# Performance and health thresholds
HIGH_UTILIZATION_THRESHOLD = 0.9 # When to warn about high utilization
LONG_TOKEN_LENGTH_THRESHOLD = 30 # Minimum length to abbreviate tokens


@dataclass
Expand Down Expand Up @@ -159,13 +158,12 @@ class UltraOptimizedStructuredLogger:
- Lock-free ring buffer
- Sampling (10% default)
- Async batch writes
- Smart PII masking
- PII key-name masking (password/token/secret/key/auth kwargs)
- Near-zero overhead when not sampled
"""

def __init__(self, name: str, mask_sensitive: bool = True):
def __init__(self, name: str):
Comment thread
27Bslash6 marked this conversation as resolved.
self.name = name
self.mask_sensitive = mask_sensitive
self.buffer = LockFreeRingBuffer()
self.writer = AsyncLogWriter(self.buffer)
self.writer.start()
Expand Down Expand Up @@ -481,29 +479,25 @@ def __exit__(self, exc_type, exc_val, exc_tb):
_logger_lock = threading.Lock()


def get_structured_logger(name: str, mask_sensitive: bool = True) -> UltraOptimizedStructuredLogger:
def get_structured_logger(name: str) -> UltraOptimizedStructuredLogger:
"""Get or create a structured logger instance.

Args:
name: Logger name (usually __name__)
mask_sensitive: Whether to mask sensitive data

Returns:
Ultra-optimized structured logger instance
"""
# Create cache key including mask_sensitive setting
cache_key = f"{name}:{mask_sensitive}"

# Fast path - check if already exists
if cache_key in _logger_instances:
return _logger_instances[cache_key]
if name in _logger_instances:
return _logger_instances[name]

# Slow path - create new instance
with _logger_lock:
# Double-check pattern
if cache_key not in _logger_instances:
_logger_instances[cache_key] = UltraOptimizedStructuredLogger(name, mask_sensitive)
return _logger_instances[cache_key]
if name not in _logger_instances:
_logger_instances[name] = UltraOptimizedStructuredLogger(name)
return _logger_instances[name]


# Alias
Expand Down Expand Up @@ -534,38 +528,3 @@ def format(self, record):
log_data["exception"] = "".join(traceback.format_exception(*record.exc_info))

return json.dumps(log_data, separators=(",", ":"))


# Additional compatibility functions
def mask_sensitive_patterns(data: str) -> str:
"""Mask sensitive patterns in data."""
if data is None:
return None

import re

# SSN patterns
data = re.sub(r"\b\d{3}-\d{2}-\d{4}\b", "XXX-XX-XXXX", data)
data = re.sub(r"\b\d{9}\b", "XXXXXXXXX", data)

# Credit card patterns
data = re.sub(r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b", "XXXX-XXXX-XXXX-XXXX", data)

# Email addresses
data = re.sub(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "XXX@XXX.XXX", data)

# Phone numbers
data = re.sub(r"\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b", "XXX-XXX-XXXX", data)
data = re.sub(r"\(\d{3}\)\s?\d{3}[-.\s]?\d{4}\b", "(XXX) XXX-XXXX", data)

# JWT tokens (must be done before general API keys)
data = re.sub(r"\b[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\b", "XXX.XXX.XXX", data)

# API keys and tokens (20+ chars)
data = re.sub(
r"\b[A-Za-z0-9_-]{20,}\b",
lambda m: "XXXXX...XXXXX" if len(m.group()) > LONG_TOKEN_LENGTH_THRESHOLD else "XXX",
data,
)

return data
14 changes: 0 additions & 14 deletions src/cachekit/reliability/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,11 @@ class ProfileConfig:
metrics_batch_size: int = 100
metrics_flush_interval: float = 0.1

# Security features
mask_sensitive_data: bool = True

# Backpressure settings
backpressure_read_operations: bool = False # Skip backpressure for reads
backpressure_timeout: float = 0.1 # Shorter timeout for cache ops

# Logging optimization
lazy_pii_masking: bool = True
log_level_threshold: str = "INFO"


Expand All @@ -94,10 +90,7 @@ class ProfileConfig:
health_cache_duration=1.0, # Cache longer for performance
# Performance optimized
max_concurrent_requests=1000, # Higher limit
# Security still enabled
mask_sensitive_data=True,
# Minimal logging
lazy_pii_masking=True,
log_level_threshold="WARNING", # Only log warnings and errors
),
ReliabilityProfile.BALANCED: ProfileConfig(
Expand All @@ -115,13 +108,10 @@ class ProfileConfig:
max_concurrent_requests=100,
metrics_batch_size=100,
metrics_flush_interval=0.1,
# Security enabled
mask_sensitive_data=True,
# Optimized backpressure
backpressure_read_operations=False, # Skip reads
backpressure_timeout=0.1,
# Optimized logging
lazy_pii_masking=True,
log_level_threshold="INFO",
),
ReliabilityProfile.FULL: ProfileConfig(
Expand All @@ -139,13 +129,10 @@ class ProfileConfig:
max_concurrent_requests=50,
metrics_batch_size=50, # Smaller batches for lower latency
metrics_flush_interval=0.05, # More frequent flushes
# Full security
mask_sensitive_data=True,
# Full backpressure (including reads)
backpressure_read_operations=True,
backpressure_timeout=0.5, # Longer timeout for reliability
# Full logging
lazy_pii_masking=True,
log_level_threshold="DEBUG",
),
}
Expand Down Expand Up @@ -234,7 +221,6 @@ def create_optimized_decorator_config(profile: ReliabilityProfile = ReliabilityP
"_use_async_metrics": config.async_metrics,
"_use_lightweight_health": True,
"_health_check_level": config.health_check_level.value,
"_lazy_pii_masking": config.lazy_pii_masking,
"_backpressure_read_ops": config.backpressure_read_operations,
"_metrics_batch_size": config.metrics_batch_size,
"_metrics_flush_interval": config.metrics_flush_interval,
Expand Down
78 changes: 11 additions & 67 deletions tests/unit/test_structured_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,72 +12,19 @@
JsonFormatter,
StructuredRedisLogger,
get_structured_logger,
mask_sensitive_patterns,
)


class TestSensitiveDataMasking:
"""Test PII masking functionality."""

def test_mask_ssn_patterns(self):
"""Test SSN masking."""
assert mask_sensitive_patterns("My SSN is 123-45-6789") == "My SSN is XXX-XX-XXXX"
assert mask_sensitive_patterns("SSN: 123456789") == "SSN: XXXXXXXXX"

def test_mask_credit_card_patterns(self):
"""Test credit card masking."""
assert mask_sensitive_patterns("Card: 1234-5678-9012-3456") == "Card: XXXX-XXXX-XXXX-XXXX"
assert mask_sensitive_patterns("Card: 1234567890123456") == "Card: XXXX-XXXX-XXXX-XXXX"

def test_mask_email_addresses(self):
"""Test email masking."""
assert mask_sensitive_patterns("Email: user@example.com") == "Email: XXX@XXX.XXX"
assert mask_sensitive_patterns("Contact: john.doe+tag@company.co.uk") == "Contact: XXX@XXX.XXX"

def test_mask_phone_numbers(self):
"""Test phone number masking."""
assert mask_sensitive_patterns("Call: 123-456-7890") == "Call: XXX-XXX-XXXX"
assert mask_sensitive_patterns("Phone: (123) 456-7890") == "Phone: (XXX) XXX-XXXX"
assert mask_sensitive_patterns("Tel: 123.456.7890") == "Tel: XXX-XXX-XXXX"

def test_mask_api_keys(self):
"""Test API key masking."""
long_key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"
assert mask_sensitive_patterns(f"API Key: {long_key}") == "API Key: XXXXX...XXXXX"

def test_mask_jwt_tokens(self):
"""Test JWT token masking."""
jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
assert mask_sensitive_patterns(f"Token: {jwt}") == "Token: XXX.XXX.XXX"

def test_mask_multiple_patterns(self):
"""Test masking multiple patterns in one string."""
text = "User email@test.com with SSN 123-45-6789 called from 555-123-4567"
expected = "User XXX@XXX.XXX with SSN XXX-XX-XXXX called from XXX-XXX-XXXX"
assert mask_sensitive_patterns(text) == expected

def test_empty_string(self):
"""Test masking empty string."""
assert mask_sensitive_patterns("") == ""
assert mask_sensitive_patterns(None) is None


class TestStructuredRedisLogger:
"""Test StructuredRedisLogger functionality."""

@pytest.fixture
def logger(self):
"""Create a test logger instance."""
return StructuredRedisLogger("test_logger", mask_sensitive=True)

@pytest.fixture
def logger_no_mask(self):
"""Create a test logger without masking."""
return StructuredRedisLogger("test_logger_no_mask", mask_sensitive=False)
return StructuredRedisLogger("test_logger")

def test_logger_initialization(self, logger):
"""Test logger initialization."""
assert logger.mask_sensitive is True
assert hasattr(logger, "_context")
assert isinstance(logger._context, threading.local)

Expand Down Expand Up @@ -106,19 +53,18 @@ def test_get_context(self, logger):
context = logger._get_context()
assert context["trace_id"] == trace_id

def test_cache_key_always_redacted(self, logger, logger_no_mask):
"""cache_operation redacts the key regardless of mask_sensitive (CWE-532, LAB-304)."""
def test_cache_key_always_redacted(self, logger):
"""cache_operation always redacts the key via digest (CWE-532, LAB-304)."""
from unittest.mock import patch as _patch

from cachekit.hash_utils import redact_cache_key

sensitive = "ns:tenant-42:func:app.f:args:email@test.com:v1"
for lg in (logger, logger_no_mask):
with _patch("cachekit.logging.logging.Logger.log") as mock_log:
lg.cache_operation("get", sensitive, hit=True)
extra = mock_log.call_args[1]["extra"]["structured"]
assert extra["cache_key"] == redact_cache_key(sensitive)
assert sensitive not in str(extra)
with _patch("cachekit.logging.logging.Logger.log") as mock_log:
logger.cache_operation("get", sensitive, hit=True)
extra = mock_log.call_args[1]["extra"]["structured"]
assert extra["cache_key"] == redact_cache_key(sensitive)
assert sensitive not in str(extra)

@patch("cachekit.logging.logging.Logger.log")
def test_cache_operation_logging(self, mock_log, logger):
Expand Down Expand Up @@ -322,11 +268,9 @@ class TestFactoryFunction:
"""Test factory function."""

def test_get_structured_logger(self):
"""Test get_structured_logger factory."""
"""Test get_structured_logger factory returns one cached instance per name."""
logger1 = get_structured_logger("test1")
assert isinstance(logger1, StructuredRedisLogger)
assert logger1.mask_sensitive is True

logger2 = get_structured_logger("test2", mask_sensitive=False)
assert isinstance(logger2, StructuredRedisLogger)
assert logger2.mask_sensitive is False
logger1_again = get_structured_logger("test1")
assert logger1_again is logger1
Loading