Logging¶
envresolve provides built-in logging support to help you debug resolution issues and monitor secret access in production.
Why Logging?¶
Logging helps you:
- Debug why a variable isn't resolving correctly
- Understand which secrets are being accessed and when
- Troubleshoot performance issues
- Audit secret access for security compliance
Basic Usage¶
With EnvResolver¶
Pass a logger to the EnvResolver constructor:
import logging
from envresolve import EnvResolver
# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Create resolver with logger
resolver = EnvResolver(logger=logger)
result = resolver.resolve_secret("${DATABASE_URL}")
With Global Facade¶
You have two options with the global facade:
Option 1: Set Global Default Logger¶
import logging
import envresolve
# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Set global default logger
envresolve.set_logger(logger)
# All subsequent calls use the global logger
result = envresolve.resolve_secret("${DATABASE_URL}")
Option 2: Override Per-Call¶
import logging
import envresolve
# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Override logger for specific call
result = envresolve.resolve_secret("${DATABASE_URL}", logger=logger)
Disabling Logging for Specific Calls¶
Important: Passing logger=None does not disable logging if a global default logger is set. Instead, the call will fall back to using the global logger.
To disable logging for a specific call when a global logger is set, temporarily clear the global logger:
import envresolve
# Set global logger
envresolve.set_logger(logger)
# This still uses the global logger (logger=None falls back to global)
result = envresolve.resolve_secret("${DATABASE_URL}", logger=None)
# To disable logging for specific calls, clear the global logger temporarily
envresolve.set_logger(None)
result = envresolve.resolve_secret("${DATABASE_URL}") # No logging
envresolve.set_logger(logger) # Restore global logger
Alternatively, use EnvResolver instances with different logger configurations:
from envresolve import EnvResolver
# Instance with logging
logged_resolver = EnvResolver(logger=logger)
# Instance without logging
silent_resolver = EnvResolver(logger=None)
# Use the appropriate instance
result = silent_resolver.resolve_secret("${DATABASE_URL}") # No logging
What Gets Logged¶
envresolve logs operations with:
- Operation type: "Variable expansion", "Secret resolution"
- Success/failure status: "completed", "failed"
- Error category: "variable not found", "circular reference detected", "provider error"
Example Log Output¶
Successful resolution:
Failed resolution:
What Does NOT Get Logged¶
By default, envresolve does not log:
- Environment variable names
- Variable names
- Vault names
- Secret names
- URIs
- Resolved values
This protects sensitive information from appearing in logs. See Security Considerations for details.
Common Logging Setups¶
Development: Console Logging¶
import logging
import envresolve
# Simple console logging for development
logging.basicConfig(
level=logging.DEBUG,
format='%(levelname)s - %(name)s - %(message)s'
)
logger = logging.getLogger(__name__)
envresolve.set_logger(logger)
Production: File Logging¶
import logging
import envresolve
# File-based logging for production
logger = logging.getLogger(__name__)
handler = logging.FileHandler('/var/log/myapp/envresolve.log')
handler.setLevel(logging.INFO)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
envresolve.set_logger(logger)
Production: JSON Logging with structlog¶
import logging
import structlog
import envresolve
# Structured JSON logging with structlog
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
logger = structlog.get_logger()
envresolve.set_logger(logger)
Production: Error-Only Logging¶
import logging
import envresolve
# Log only errors in production
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
handler.setLevel(logging.ERROR)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.ERROR)
envresolve.set_logger(logger)
Testing: Capturing Logs¶
import logging
from envresolve import EnvResolver
def test_secret_resolution():
# Use pytest's caplog fixture or Python's logging.handlers.MemoryHandler
logger = logging.getLogger("test")
handler = logging.handlers.MemoryHandler(capacity=100)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
resolver = EnvResolver(logger=logger)
result = resolver.resolve_secret("plain-text")
# Verify logs
handler.flush()
records = handler.buffer
assert any("Variable expansion completed" in r.message for r in records)
Per-Method Logger Override¶
You can override the logger for individual method calls:
import logging
from envresolve import EnvResolver
# Constructor logger
default_logger = logging.getLogger("default")
resolver = EnvResolver(logger=default_logger)
# Override for specific call
special_logger = logging.getLogger("special")
result = resolver.resolve_secret("${SECRET}", logger=special_logger)
Security Considerations¶
What's Safe to Log¶
envresolve's default logging is designed to be safe for general-purpose logging systems that may:
- Be stored in less secure environments
- Be accessible to multiple teams
- Be retained long-term
- Be shared for debugging
The operation-level logging (type, status, error category) provides enough information for:
- Monitoring and alerting
- Detecting configuration issues
- Troubleshooting resolution failures
What's NOT Safe by Default¶
By design, envresolve does not log:
- Variable names (could reveal
ADMIN_PASSWORD,PROD_API_KEY) - Vault names (could reveal infrastructure:
prod-vault,staging-vault) - Secret names (could reveal what secrets exist)
- URIs (could reveal secret locations)
- Resolved values (would expose secrets themselves)
Exception Details¶
While logs are minimal, exception objects contain full details:
import envresolve
try:
result = envresolve.resolve_secret("${MISSING_VAR}")
except envresolve.VariableNotFoundError as e:
# Exception has full details
print(f"Variable not found: {e.variable_name}")
# You can decide what to log
logger.error(f"Resolution failed for ${{{e.variable_name}}}")
This lets you implement custom logging based on your security requirements.
Application-Level Logging¶
For more detailed logging, catch exceptions and log at the application level:
import logging
import envresolve
logger = logging.getLogger(__name__)
try:
result = envresolve.resolve_secret("${DATABASE_URL}")
except envresolve.VariableNotFoundError as e:
# Log with context appropriate for your security policy
logger.error(
"Failed to resolve environment variable",
extra={
"variable": e.variable_name,
"context": "database_config"
}
)
raise
Advanced: Correlation IDs¶
Add correlation IDs using logging filters:
import logging
import contextvars
import envresolve
# Context variable for request ID
request_id_var = contextvars.ContextVar('request_id', default=None)
class RequestIdFilter(logging.Filter):
def filter(self, record):
record.request_id = request_id_var.get()
return True
# Set up logger with filter
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
handler.addFilter(RequestIdFilter())
formatter = logging.Formatter(
'%(asctime)s - %(request_id)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
envresolve.set_logger(logger)
# Use in request handler
def handle_request(request):
request_id_var.set(request.id)
config = envresolve.load_env()
# Logs will include request_id
Performance¶
Logging has zero performance impact when no logger is configured (no per-call logger, no constructor logger, and no global default logger). The implementation uses simple if logger is not None checks that have negligible overhead.
Logger Resolution Order¶
The logger resolution order depends on which API you use:
For EnvResolver Instances¶
When using EnvResolver instances:
- Per-call logger parameter (e.g.,
resolver.resolve_secret(uri, logger=my_logger)) - Constructor logger (e.g.,
EnvResolver(logger=my_logger)) - No logging if neither is provided
Note: The global default logger set via set_logger() does NOT affect EnvResolver instances. Each instance is independent.
For Global Facade Functions¶
When using module-level functions (e.g., envresolve.resolve_secret()):
- Per-call logger parameter (e.g.,
envresolve.resolve_secret(uri, logger=my_logger)) - Global default logger (set via
envresolve.set_logger(my_logger)) - No logging if neither is provided
This means:
EnvResolverinstances:logger=Nonefalls back to constructor logger, NOT global logger- Global functions:
logger=Nonefalls back to global default logger - Per-call
logger=Nonedoes not disable logging if a fallback logger exists - To ensure zero performance impact: don't set global logger AND don't pass logger parameters