From 59a04f4dc582ae3c2703d4969f4eec7cb021e779 Mon Sep 17 00:00:00 2001 From: Nikhil172913832 Date: Thu, 20 Nov 2025 15:26:28 +0530 Subject: [PATCH 1/5] feat(azure-core): Add configurable logging level to HttpLoggingPolicy - Add logging_level parameter to HttpLoggingPolicy.__init__ with default logging.INFO - Replace hardcoded logging.INFO checks and logger.info() calls with configurable level - Add tests for custom log level in both sync and async test suites - Maintain backward compatibility with default INFO level --- .../core/pipeline/policies/_universal.py | 40 +++++++++-------- .../test_http_logging_policy_async.py | 44 +++++++++++++++++++ .../tests/test_http_logging_policy.py | 44 +++++++++++++++++++ 3 files changed, 109 insertions(+), 19 deletions(-) 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..0aa3037b7ce9 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,7 @@ 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. """ DEFAULT_HEADERS_ALLOWLIST: Set[str] = set( @@ -425,8 +426,9 @@ 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 +455,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 +468,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 +496,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 +537,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): From ec23ecb9d3a0d134c27e7dfba7738e407452dc67 Mon Sep 17 00:00:00 2001 From: Nikhil Arora Date: Thu, 20 Nov 2025 15:42:33 +0530 Subject: [PATCH 2/5] Update sdk/core/azure-core/azure/core/pipeline/policies/_universal.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sdk/core/azure-core/azure/core/pipeline/policies/_universal.py | 1 + 1 file changed, 1 insertion(+) 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 0aa3037b7ce9..a461d557e272 100644 --- a/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py +++ b/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py @@ -390,6 +390,7 @@ 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( From cb385b5c1fef07387b55a926f4c06126bbc09e68 Mon Sep 17 00:00:00 2001 From: Nikhil172913832 Date: Thu, 20 Nov 2025 16:06:51 +0530 Subject: [PATCH 3/5] fixed the formatting issues causing ci failure --- .../azure-core/azure/core/pipeline/policies/_universal.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 a461d557e272..8bf302c03bc0 100644 --- a/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py +++ b/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py @@ -427,7 +427,9 @@ class HttpLoggingPolicy( REDACTED_PLACEHOLDER: str = "REDACTED" MULTI_RECORD_LOG: str = "AZURE_SDK_LOGGING_MULTIRECORD" - def __init__(self, logger: Optional[logging.Logger] = None, *, logging_level: int = logging.INFO, **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() From b5eec3cfeaf3356e4500132bdf02ff7468831552 Mon Sep 17 00:00:00 2001 From: Nikhil172913832 Date: Tue, 2 Dec 2025 15:22:18 +0530 Subject: [PATCH 4/5] Change the paramter name from logging_level to http_logging_level --- .../core/pipeline/policies/_universal.py | 44 +++++++++---------- .../test_http_logging_policy_async.py | 2 +- .../tests/test_http_logging_policy.py | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) 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 8bf302c03bc0..592c895a4c68 100644 --- a/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py +++ b/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py @@ -389,8 +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 + :keyword int http_logging_level: The logging level to use for HTTP request and response logs. Defaults to logging.INFO. + :type http_logging_level: int """ DEFAULT_HEADERS_ALLOWLIST: Set[str] = set( @@ -428,10 +428,10 @@ class HttpLoggingPolicy( MULTI_RECORD_LOG: str = "AZURE_SDK_LOGGING_MULTIRECORD" def __init__( - self, logger: Optional[logging.Logger] = None, *, logging_level: int = logging.INFO, **kwargs: Any + self, logger: Optional[logging.Logger] = None, *, http_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.http_logging_level: int = http_logging_level self.allowed_query_params: Set[str] = set() self.allowed_header_names: Set[str] = set(self.__class__.DEFAULT_HEADERS_ALLOWLIST) @@ -458,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(self.logging_level): + if not logger.isEnabledFor(self.http_logging_level): return try: @@ -471,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.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:") + logger.log(self.http_logging_level, "Request URL: %r", redacted_url) + logger.log(self.http_logging_level, "Request method: %r", http_request.method) + logger.log(self.http_logging_level, "Request headers:") for header, value in http_request.headers.items(): value = self._redact_header(header, value) - logger.log(self.logging_level, " %r: %r", header, value) + logger.log(self.http_logging_level, " %r: %r", header, value) if isinstance(http_request.body, types.GeneratorType): - logger.log(self.logging_level, "File upload") + logger.log(self.http_logging_level, "File upload") return try: if isinstance(http_request.body, types.AsyncGeneratorType): - logger.log(self.logging_level, "File upload") + logger.log(self.http_logging_level, "File upload") return except AttributeError: pass if http_request.body: - logger.log(self.logging_level, "A body is sent with the request") + logger.log(self.http_logging_level, "A body is sent with the request") return - logger.log(self.logging_level, "No body was attached to the request") + logger.log(self.http_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) @@ -499,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.log(self.logging_level, log_string) + logger.log(self.http_logging_level, log_string) return try: if isinstance(http_request.body, types.AsyncGeneratorType): log_string += "\nFile upload" - logger.log(self.logging_level, log_string) + logger.log(self.http_logging_level, log_string) return except AttributeError: pass if http_request.body: log_string += "\nA body is sent with the request" - logger.log(self.logging_level, log_string) + logger.log(self.http_logging_level, log_string) return log_string += "\nNo body was attached to the request" - logger.log(self.logging_level, log_string) + logger.log(self.http_logging_level, log_string) except Exception: # pylint: disable=broad-except logger.warning("Failed to log request.") @@ -540,23 +540,23 @@ def on_response( logger = request.context.setdefault("logger", options.pop("logger", self.logger)) try: - if not logger.isEnabledFor(self.logging_level): + if not logger.isEnabledFor(self.http_logging_level): return multi_record = os.environ.get(HttpLoggingPolicy.MULTI_RECORD_LOG, False) if multi_record: - logger.log(self.logging_level, "Response status: %r", http_response.status_code) - logger.log(self.logging_level, "Response headers:") + logger.log(self.http_logging_level, "Response status: %r", http_response.status_code) + logger.log(self.http_logging_level, "Response headers:") for res_header, value in http_response.headers.items(): value = self._redact_header(res_header, value) - logger.log(self.logging_level, " %r: %r", res_header, value) + logger.log(self.http_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.log(self.logging_level, log_string) + logger.log(self.http_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 bb040b835f23..4666d6d7fcc2 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 @@ -274,7 +274,7 @@ def emit(self, record): logger.addHandler(mock_handler) logger.setLevel(logging.DEBUG) - policy = HttpLoggingPolicy(logger=logger, logging_level=logging.DEBUG) + policy = HttpLoggingPolicy(logger=logger, http_logging_level=logging.DEBUG) universal_request = http_request("GET", "http://localhost/") http_response = create_http_response(http_response, universal_request, None) 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 2b9f7662a921..63cfd118ce9e 100644 --- a/sdk/core/azure-core/tests/test_http_logging_policy.py +++ b/sdk/core/azure-core/tests/test_http_logging_policy.py @@ -279,7 +279,7 @@ def emit(self, record): logger.addHandler(mock_handler) logger.setLevel(logging.DEBUG) - policy = HttpLoggingPolicy(logger=logger, logging_level=logging.DEBUG) + policy = HttpLoggingPolicy(logger=logger, http_logging_level=logging.DEBUG) universal_request = http_request("GET", "http://localhost/") http_response = create_http_response(http_response, universal_request, None) From 006a8ebe41bbecca05e5be79e8c0742b5753efaf Mon Sep 17 00:00:00 2001 From: Nikhil172913832 Date: Tue, 2 Dec 2025 15:57:26 +0530 Subject: [PATCH 5/5] minor formatting change as per the ci failure --- sdk/core/azure-core/azure/core/pipeline/policies/_universal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 592c895a4c68..bfcbd3ceeb44 100644 --- a/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py +++ b/sdk/core/azure-core/azure/core/pipeline/policies/_universal.py @@ -389,7 +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 http_logging_level: The logging level to use for HTTP request and response logs. Defaults to logging.INFO. + :keyword int http_logging_level: The logging level to use for HTTP request and response logs. + Defaults to logging.INFO. :type http_logging_level: int """