Skip to content

Test Logging and Secret Masking

★★★★★ Intermediate

Structured logging in test frameworks, masking sensitive data in logs and reports, and DevTools techniques for test debugging.

Python Logging in Tests

import logging

logger = logging.getLogger(__name__)

@pytest.fixture(autouse=True)
def configure_logging(caplog):
    caplog.set_level(logging.DEBUG)

def test_api_call(api_client, caplog):
    resp = api_client.get("/api/users")
    assert resp.status_code == 200

    # Verify logging happened
    assert "GET /api/users" in caplog.text
    assert "200" in caplog.text

HTTP Request/Response Logging

import requests
import logging

def setup_request_logging(session):
    """Log all HTTP requests made by this session."""
    def log_request(resp, *args, **kwargs):
        logger.info(f"{resp.request.method} {resp.request.url} -> {resp.status_code}")
        logger.debug(f"Request headers: {resp.request.headers}")
        logger.debug(f"Response body: {resp.text[:500]}")

    session.hooks["response"].append(log_request)
    return session

Secret Masking in Logs

import re
import logging

class SensitiveDataFilter(logging.Filter):
    PATTERNS = [
        (re.compile(r'(Bearer\s+)\S+'), r'\1***MASKED***'),
        (re.compile(r'(password["\s:=]+)\S+', re.I), r'\1***MASKED***'),
        (re.compile(r'(token["\s:=]+)\S+', re.I), r'\1***MASKED***'),
        (re.compile(r'(api[_-]?key["\s:=]+)\S+', re.I), r'\1***MASKED***')]

    def filter(self, record):
        msg = record.getMessage()
        for pattern, replacement in self.PATTERNS:
            msg = pattern.sub(replacement, msg)
        record.msg = msg
        record.args = ()
        return True

# Apply globally
logging.getLogger().addFilter(SensitiveDataFilter())

Masking in Allure Reports

import allure
import re

MASK_PATTERNS = [
    re.compile(r'"(password|token|secret|api_key)":\s*"[^"]*"')]

def safe_attach(content, name, attachment_type=allure.attachment_type.TEXT):
    """Attach content to Allure with sensitive data masked."""
    masked = content
    for pattern in MASK_PATTERNS:
        masked = pattern.sub(lambda m: m.group().split(":")[0] + ': "***"', masked)
    allure.attach(masked, name, attachment_type)

Custom Allure Logging for Requests

class AllureLoggingSession(requests.Session):
    def request(self, method, url, **kwargs):
        response = super().request(method, url, **kwargs)

        with allure.step(f"{method.upper()} {url}"):
            # Mask headers
            safe_headers = dict(response.request.headers)
            if "Authorization" in safe_headers:
                safe_headers["Authorization"] = "Bearer ***"

            safe_attach(str(safe_headers), "Request Headers")

            if kwargs.get("json"):
                body = json.dumps(kwargs["json"], indent=2)
                safe_attach(body, "Request Body")

            safe_attach(
                response.text[:2000],
                f"Response [{response.status_code}]"
            )

        return response

Environment Variable Protection

# conftest.py
import os

@pytest.fixture(autouse=True, scope="session")
def protect_env_vars():
    """Ensure sensitive env vars exist but warn if missing."""
    required = ["TEST_API_TOKEN", "TEST_DB_PASSWORD"]
    missing = [v for v in required if not os.getenv(v)]
    if missing:
        pytest.skip(f"Missing env vars: {missing}")

    # Log that we have them (not their values)
    for var in required:
        logger.info(f"Env var {var}: {'set' if os.getenv(var) else 'missing'}")

DevTools for UI Test Debugging

Browser DevTools from test code:

# Playwright
page.on("console", lambda msg: logger.debug(f"Browser: {msg.text}"))
page.on("pageerror", lambda err: logger.error(f"Page error: {err}"))

# Selenium
logs = driver.get_log("browser")
for entry in logs:
    logger.debug(f"Browser [{entry['level']}]: {entry['message']}")

Gotchas

  • Issue: Allure report contains raw Bearer tokens in request headers visible to entire team. Fix: Always use masking session wrapper. Add a CI check that greps Allure output for token-like patterns.

  • Issue: logging.basicConfig() called in conftest conflicts with pytest's log capture. Fix: Use caplog fixture or configure via pytest.ini: log_cli = true, log_cli_level = DEBUG.

  • Issue: Masked logs make debugging impossible when the actual token/value is needed. Fix: Use a --debug-secrets CLI flag that disables masking in local runs only. Never allow in CI.

See Also