diff --git a/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py b/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py index 67fc6aef4e69..8bf302c03bc0 100644 --- a/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py +++ b/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py @@ -389,6 +389,8 @@ class HttpLoggingPolicy( :param logger: The logger to use for logging. Default to azure.core.pipeline.policies.http_logging_policy. :type logger: logging.Logger + :keyword int logging_level: The logging level to use for request and response logs. Defaults to logging.INFO. + :type logging_level: int """ DEFAULT_HEADERS_ALLOWLIST: Set[str] = set( @@ -425,8 +427,11 @@ class HttpLoggingPolicy( REDACTED_PLACEHOLDER: str = "REDACTED" MULTI_RECORD_LOG: str = "AZURE_SDK_LOGGING_MULTIRECORD" - def __init__(self, logger: Optional[logging.Logger] = None, **kwargs: Any): # pylint: disable=unused-argument + def __init__( + self, logger: Optional[logging.Logger] = None, *, logging_level: int = logging.INFO, **kwargs: Any + ): # pylint: disable=unused-argument self.logger: logging.Logger = logger or logging.getLogger("azure.core.pipeline.policies.http_logging_policy") + self.logging_level: int = logging_level self.allowed_query_params: Set[str] = set() self.allowed_header_names: Set[str] = set(self.__class__.DEFAULT_HEADERS_ALLOWLIST) @@ -453,7 +458,7 @@ def on_request( # pylint: disable=too-many-return-statements # then use my instance logger logger = request.context.setdefault("logger", options.pop("logger", self.logger)) - if not logger.isEnabledFor(logging.INFO): + if not logger.isEnabledFor(self.logging_level): return try: @@ -466,25 +471,25 @@ def on_request( # pylint: disable=too-many-return-statements multi_record = os.environ.get(HttpLoggingPolicy.MULTI_RECORD_LOG, False) if multi_record: - logger.info("Request URL: %r", redacted_url) - logger.info("Request method: %r", http_request.method) - logger.info("Request headers:") + logger.log(self.logging_level, "Request URL: %r", redacted_url) + logger.log(self.logging_level, "Request method: %r", http_request.method) + logger.log(self.logging_level, "Request headers:") for header, value in http_request.headers.items(): value = self._redact_header(header, value) - logger.info(" %r: %r", header, value) + logger.log(self.logging_level, " %r: %r", header, value) if isinstance(http_request.body, types.GeneratorType): - logger.info("File upload") + logger.log(self.logging_level, "File upload") return try: if isinstance(http_request.body, types.AsyncGeneratorType): - logger.info("File upload") + logger.log(self.logging_level, "File upload") return except AttributeError: pass if http_request.body: - logger.info("A body is sent with the request") + logger.log(self.logging_level, "A body is sent with the request") return - logger.info("No body was attached to the request") + logger.log(self.logging_level, "No body was attached to the request") return log_string = "Request URL: '{}'".format(redacted_url) log_string += "\nRequest method: '{}'".format(http_request.method) @@ -494,21 +499,21 @@ def on_request( # pylint: disable=too-many-return-statements log_string += "\n '{}': '{}'".format(header, value) if isinstance(http_request.body, types.GeneratorType): log_string += "\nFile upload" - logger.info(log_string) + logger.log(self.logging_level, log_string) return try: if isinstance(http_request.body, types.AsyncGeneratorType): log_string += "\nFile upload" - logger.info(log_string) + logger.log(self.logging_level, log_string) return except AttributeError: pass if http_request.body: log_string += "\nA body is sent with the request" - logger.info(log_string) + logger.log(self.logging_level, log_string) return log_string += "\nNo body was attached to the request" - logger.info(log_string) + logger.log(self.logging_level, log_string) except Exception: # pylint: disable=broad-except logger.warning("Failed to log request.") @@ -535,23 +540,23 @@ def on_response( logger = request.context.setdefault("logger", options.pop("logger", self.logger)) try: - if not logger.isEnabledFor(logging.INFO): + if not logger.isEnabledFor(self.logging_level): return multi_record = os.environ.get(HttpLoggingPolicy.MULTI_RECORD_LOG, False) if multi_record: - logger.info("Response status: %r", http_response.status_code) - logger.info("Response headers:") + logger.log(self.logging_level, "Response status: %r", http_response.status_code) + logger.log(self.logging_level, "Response headers:") for res_header, value in http_response.headers.items(): value = self._redact_header(res_header, value) - logger.info(" %r: %r", res_header, value) + logger.log(self.logging_level, " %r: %r", res_header, value) return log_string = "Response status: {}".format(http_response.status_code) log_string += "\nResponse headers:" for res_header, value in http_response.headers.items(): value = self._redact_header(res_header, value) log_string += "\n '{}': '{}'".format(res_header, value) - logger.info(log_string) + logger.log(self.logging_level, log_string) except Exception: # pylint: disable=broad-except logger.warning("Failed to log response.") diff --git a/sdk/core/azure-core/tests/async_tests/test_http_logging_policy_async.py b/sdk/core/azure-core/tests/async_tests/test_http_logging_policy_async.py index 0f28c70c2c4b..bb040b835f23 100644 --- a/sdk/core/azure-core/tests/async_tests/test_http_logging_policy_async.py +++ b/sdk/core/azure-core/tests/async_tests/test_http_logging_policy_async.py @@ -255,6 +255,50 @@ def emit(self, record): mock_handler.reset() +@pytest.mark.parametrize("http_request,http_response", request_and_responses_product(HTTP_RESPONSES)) +def test_http_logger_with_custom_log_level(http_request, http_response): + class MockHandler(logging.Handler): + def __init__(self): + super(MockHandler, self).__init__() + self.messages = [] + + def reset(self): + self.messages = [] + + def emit(self, record): + self.messages.append(record) + + mock_handler = MockHandler() + + logger = logging.getLogger("testlogger") + logger.addHandler(mock_handler) + logger.setLevel(logging.DEBUG) + + policy = HttpLoggingPolicy(logger=logger, logging_level=logging.DEBUG) + + universal_request = http_request("GET", "http://localhost/") + http_response = create_http_response(http_response, universal_request, None) + http_response.status_code = 202 + request = PipelineRequest(universal_request, PipelineContext(None)) + + policy.on_request(request) + response = PipelineResponse(request, http_response, request.context) + policy.on_response(request, response) + + assert all(m.levelname == "DEBUG" for m in mock_handler.messages) + assert len(mock_handler.messages) == 2 + messages_request = mock_handler.messages[0].message.split("\n") + messages_response = mock_handler.messages[1].message.split("\n") + assert messages_request[0] == "Request URL: 'http://localhost/'" + assert messages_request[1] == "Request method: 'GET'" + assert messages_request[2] == "Request headers:" + assert messages_request[3] == "No body was attached to the request" + assert messages_response[0] == "Response status: 202" + assert messages_response[1] == "Response headers:" + + mock_handler.reset() + + @pytest.mark.parametrize("http_request,http_response", request_and_responses_product(HTTP_RESPONSES)) @pytest.mark.skipif(sys.version_info < (3, 6), reason="types.AsyncGeneratorType does not exist in 3.5") def test_http_logger_with_generator_body(http_request, http_response): diff --git a/sdk/core/azure-core/tests/test_http_logging_policy.py b/sdk/core/azure-core/tests/test_http_logging_policy.py index dfc397fa1da6..2b9f7662a921 100644 --- a/sdk/core/azure-core/tests/test_http_logging_policy.py +++ b/sdk/core/azure-core/tests/test_http_logging_policy.py @@ -260,6 +260,50 @@ def emit(self, record): mock_handler.reset() +@pytest.mark.parametrize("http_request,http_response", request_and_responses_product(HTTP_RESPONSES)) +def test_http_logger_with_custom_log_level(http_request, http_response): + class MockHandler(logging.Handler): + def __init__(self): + super(MockHandler, self).__init__() + self.messages = [] + + def reset(self): + self.messages = [] + + def emit(self, record): + self.messages.append(record) + + mock_handler = MockHandler() + + logger = logging.getLogger("testlogger") + logger.addHandler(mock_handler) + logger.setLevel(logging.DEBUG) + + policy = HttpLoggingPolicy(logger=logger, logging_level=logging.DEBUG) + + universal_request = http_request("GET", "http://localhost/") + http_response = create_http_response(http_response, universal_request, None) + http_response.status_code = 202 + request = PipelineRequest(universal_request, PipelineContext(None)) + + policy.on_request(request) + response = PipelineResponse(request, http_response, request.context) + policy.on_response(request, response) + + assert all(m.levelname == "DEBUG" for m in mock_handler.messages) + assert len(mock_handler.messages) == 2 + messages_request = mock_handler.messages[0].message.split("\n") + messages_response = mock_handler.messages[1].message.split("\n") + assert messages_request[0] == "Request URL: 'http://localhost/'" + assert messages_request[1] == "Request method: 'GET'" + assert messages_request[2] == "Request headers:" + assert messages_request[3] == "No body was attached to the request" + assert messages_response[0] == "Response status: 202" + assert messages_response[1] == "Response headers:" + + mock_handler.reset() + + @pytest.mark.parametrize("http_request,http_response", request_and_responses_product(HTTP_RESPONSES)) def test_http_logger_with_generator_body(http_request, http_response): class MockHandler(logging.Handler):