Skip to content

Commit 304eea8

Browse files
jeremydvossCopilot
andauthored
Add error handling for diagnostic logging setup (#42505)
* copilot tests * simplify tests * tests for second logger * changelog * changelog * Update sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_diagnostics/diagnostic_logging.py Co-authored-by: Copilot <[email protected]> * lint * singleton * lint newline * feedback: Add check to avoid lock expense, pass instead of info. --------- Co-authored-by: Copilot <[email protected]>
1 parent b79aedf commit 304eea8

File tree

3 files changed

+136
-21
lines changed

3 files changed

+136
-21
lines changed

sdk/monitor/azure-monitor-opentelemetry/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
### Breaking Changes
1010

1111
### Bugs Fixed
12+
1213
- Fixed issue #42337, removes warning messages for instrumentations that target multiple packages. The logic for dependency conflict detection has been enhanced by adding "instruments-any" feature. This feature is used when an instrumentation requires any of a set of dependencies rather than all. Follows upstream dependency conflict detection logic - https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py
1314
([#42342](https://github.com/Azure/azure-sdk-for-python/pull/42342))
1415

16+
- Add error handling for diagnostic logging setup
17+
([#42505](https://github.com/Azure/azure-sdk-for-python/pull/42505))
18+
1519
### Other Changes
1620

1721
## 1.6.13 (2025-07-30)

sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_diagnostics/diagnostic_logging.py

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@
1818
)
1919
from azure.monitor.opentelemetry._version import VERSION
2020

21+
# This logger is used for logging messages about the setup of AzureDiagnosticLogging
22+
_logger = logging.getLogger("azure.monitor.opentelemetry._diagnostics")
23+
2124
_DIAGNOSTIC_LOGGER_FILE_NAME = "applicationinsights-extension.log"
2225
_SITE_NAME = _env_var_or_default("WEBSITE_SITE_NAME")
2326
_SUBSCRIPTION_ID_ENV_VAR = _env_var_or_default("WEBSITE_OWNER_NAME")
2427
_SUBSCRIPTION_ID = _SUBSCRIPTION_ID_ENV_VAR.split("+")[0] if _SUBSCRIPTION_ID_ENV_VAR else None
25-
_logger = logging.getLogger(__name__)
26-
_logger.propagate = False
27-
_logger.setLevel(logging.DEBUG)
28+
# This logger is used for logging the diagnostic logs themselves to a log file
29+
_diagnostic_file_logger = logging.getLogger(__name__)
30+
_diagnostic_file_logger.propagate = False
31+
_diagnostic_file_logger.setLevel(logging.DEBUG)
2832
_DIAGNOSTIC_LOG_PATH = _get_log_path()
2933

3034
_DISTRO_DETECTS_ATTACH = "4100"
@@ -44,13 +48,21 @@
4448

4549

4650
class AzureDiagnosticLogging:
51+
_instance = None
4752
_initialized = False
4853
_lock = threading.Lock()
4954

55+
def __new__(cls):
56+
if cls._instance is None:
57+
with cls._lock:
58+
if cls._instance is None:
59+
cls._instance = super(AzureDiagnosticLogging, cls).__new__(cls)
60+
return cls._instance
61+
5062
@classmethod
5163
def _initialize(cls):
52-
with AzureDiagnosticLogging._lock:
53-
if not AzureDiagnosticLogging._initialized:
64+
with cls._lock:
65+
if not cls._initialized:
5466
if _is_diagnostics_enabled() and _DIAGNOSTIC_LOG_PATH:
5567
log_format = (
5668
"{"
@@ -70,30 +82,46 @@ def _initialize(cls):
7082
+ "}"
7183
+ "}"
7284
)
73-
if not exists(_DIAGNOSTIC_LOG_PATH):
74-
makedirs(_DIAGNOSTIC_LOG_PATH)
75-
f_handler = logging.FileHandler(join(_DIAGNOSTIC_LOG_PATH, _DIAGNOSTIC_LOGGER_FILE_NAME))
76-
formatter = logging.Formatter(fmt=log_format, datefmt="%Y-%m-%dT%H:%M:%S")
77-
f_handler.setFormatter(formatter)
78-
_logger.addHandler(f_handler)
79-
AzureDiagnosticLogging._initialized = True
85+
try:
86+
if not exists(_DIAGNOSTIC_LOG_PATH):
87+
try:
88+
makedirs(_DIAGNOSTIC_LOG_PATH)
89+
# Multi-thread can create a race condition for creating the log file
90+
except FileExistsError:
91+
pass
92+
f_handler = logging.FileHandler(join(_DIAGNOSTIC_LOG_PATH, _DIAGNOSTIC_LOGGER_FILE_NAME))
93+
formatter = logging.Formatter(fmt=log_format, datefmt="%Y-%m-%dT%H:%M:%S")
94+
f_handler.setFormatter(formatter)
95+
_diagnostic_file_logger.addHandler(f_handler)
96+
cls._initialized = True
97+
except Exception as e: # pylint: disable=broad-except
98+
_logger.error("Failed to initialize Azure Monitor diagnostic logging: %s", e)
99+
cls._initialized = False
80100

81101
@classmethod
82102
def debug(cls, message: str, message_id: str):
83-
AzureDiagnosticLogging._initialize()
84-
_logger.debug(message, extra={"msgId": message_id})
103+
if not cls._initialized:
104+
cls._initialize()
105+
if cls._initialized:
106+
_diagnostic_file_logger.debug(message, extra={"msgId": message_id})
85107

86108
@classmethod
87109
def info(cls, message: str, message_id: str):
88-
AzureDiagnosticLogging._initialize()
89-
_logger.info(message, extra={"msgId": message_id})
110+
if not cls._initialized:
111+
cls._initialize()
112+
if cls._initialized:
113+
_diagnostic_file_logger.info(message, extra={"msgId": message_id})
90114

91115
@classmethod
92116
def warning(cls, message: str, message_id: str):
93-
AzureDiagnosticLogging._initialize()
94-
_logger.warning(message, extra={"msgId": message_id})
117+
if not cls._initialized:
118+
cls._initialize()
119+
if cls._initialized:
120+
_diagnostic_file_logger.warning(message, extra={"msgId": message_id})
95121

96122
@classmethod
97123
def error(cls, message: str, message_id: str):
98-
AzureDiagnosticLogging._initialize()
99-
_logger.error(message, extra={"msgId": message_id})
124+
if not cls._initialized:
125+
cls._initialize()
126+
if cls._initialized:
127+
_diagnostic_file_logger.error(message, extra={"msgId": message_id})

sdk/monitor/azure-monitor-opentelemetry/tests/diagnostics/test_diagnostic_logging.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def set_up(
6161
is_diagnostics_enabled,
6262
subscription_id_env_var=TEST_SUBSCRIPTION_ID_PLUS,
6363
) -> None:
64-
diagnostic_logger._logger.handlers.clear()
64+
diagnostic_logger._diagnostic_file_logger.handlers.clear()
6565
patch.dict(
6666
"os.environ",
6767
{
@@ -165,3 +165,86 @@ def test_subscription_id_no_plus(self, temp_file_path):
165165
diagnostic_logger.AzureDiagnosticLogging.info(MESSAGE1, "4200")
166166
diagnostic_logger.AzureDiagnosticLogging.info(MESSAGE2, "4301")
167167
check_file_for_messages(temp_file_path, "INFO", ((MESSAGE1, "4200"), (MESSAGE2, "4301")))
168+
169+
def test_initialize_file_handler_exception(self, temp_file_path):
170+
"""Test that initialization fails gracefully when FileHandler creation raises an exception."""
171+
set_up(temp_file_path, is_diagnostics_enabled=True)
172+
# Mock FileHandler to raise an exception
173+
with patch("azure.monitor.opentelemetry._diagnostics.diagnostic_logging.logging.FileHandler") as mock_file_handler, \
174+
patch("azure.monitor.opentelemetry._diagnostics.diagnostic_logging._logger") as mock_logger:
175+
mock_file_handler.side_effect = OSError("Permission denied")
176+
# Attempt to log, which will trigger initialization
177+
diagnostic_logger.AzureDiagnosticLogging.info(MESSAGE1, "4200")
178+
# Verify that initialization failed
179+
assert diagnostic_logger.AzureDiagnosticLogging._initialized is False
180+
check_file_is_empty(temp_file_path)
181+
# Verify that the error was logged
182+
mock_logger.error.assert_called_once()
183+
184+
def test_initialize_makedirs_exception_not_file_exists(self, temp_file_path):
185+
"""Test that initialization fails gracefully when makedirs raises a non-FileExistsError exception."""
186+
set_up(temp_file_path, is_diagnostics_enabled=True)
187+
# Mock makedirs to raise a PermissionError
188+
with patch("azure.monitor.opentelemetry._diagnostics.diagnostic_logging.makedirs") as mock_makedirs, \
189+
patch("azure.monitor.opentelemetry._diagnostics.diagnostic_logging.exists", return_value=False), \
190+
patch("azure.monitor.opentelemetry._diagnostics.diagnostic_logging._logger") as mock_logger:
191+
mock_makedirs.side_effect = PermissionError("Permission denied")
192+
# Attempt to log, which will trigger initialization
193+
diagnostic_logger.AzureDiagnosticLogging.info(MESSAGE1, "4200")
194+
# Verify that initialization failed
195+
assert diagnostic_logger.AzureDiagnosticLogging._initialized is False
196+
check_file_is_empty(temp_file_path)
197+
# Verify that the error was logged
198+
mock_logger.error.assert_called_once()
199+
200+
def test_initialize_makedirs_file_exists_error_handled(self, temp_file_path):
201+
"""Test that FileExistsError from makedirs is handled gracefully and initialization continues."""
202+
set_up(temp_file_path, is_diagnostics_enabled=True)
203+
# Mock makedirs to raise FileExistsError (this should be handled gracefully)
204+
with patch("azure.monitor.opentelemetry._diagnostics.diagnostic_logging.makedirs") as mock_makedirs, \
205+
patch("azure.monitor.opentelemetry._diagnostics.diagnostic_logging.exists", return_value=False), \
206+
patch("azure.monitor.opentelemetry._diagnostics.diagnostic_logging._logger") as mock_logger:
207+
mock_makedirs.side_effect = FileExistsError("Directory already exists")
208+
# Attempt to log, which will trigger initialization
209+
diagnostic_logger.AzureDiagnosticLogging.info(MESSAGE1, "4200")
210+
# Verify that initialization succeeded despite FileExistsError
211+
assert diagnostic_logger.AzureDiagnosticLogging._initialized is True
212+
check_file_for_messages(temp_file_path, "INFO", ((MESSAGE1, "4200"),))
213+
214+
def test_initialize_formatter_exception(self, temp_file_path):
215+
"""Test that initialization fails gracefully when Formatter creation raises an exception."""
216+
set_up(temp_file_path, is_diagnostics_enabled=True)
217+
# Mock Formatter to raise an exception
218+
with patch("azure.monitor.opentelemetry._diagnostics.diagnostic_logging.logging.Formatter") as mock_formatter, \
219+
patch("azure.monitor.opentelemetry._diagnostics.diagnostic_logging._logger") as mock_logger:
220+
mock_formatter.side_effect = ValueError("Invalid format string")
221+
# Attempt to log, which will trigger initialization
222+
diagnostic_logger.AzureDiagnosticLogging.info(MESSAGE1, "4200")
223+
# Verify that initialization failed
224+
assert diagnostic_logger.AzureDiagnosticLogging._initialized is False
225+
# Verify that the error was logged
226+
mock_logger.error.assert_called_once()
227+
228+
def test_singleton_pattern(self, temp_file_path):
229+
"""Test that AzureDiagnosticLogging follows the singleton pattern."""
230+
set_up(temp_file_path, is_diagnostics_enabled=True)
231+
232+
# Create multiple instances
233+
instance1 = diagnostic_logger.AzureDiagnosticLogging()
234+
instance2 = diagnostic_logger.AzureDiagnosticLogging()
235+
instance3 = diagnostic_logger.AzureDiagnosticLogging()
236+
237+
# Verify all instances are the same object
238+
assert instance1 is instance2
239+
assert instance2 is instance3
240+
assert instance1 is instance3
241+
242+
# Verify they all have the same id (memory address)
243+
assert id(instance1) == id(instance2) == id(instance3)
244+
245+
# Verify class-level access still works
246+
diagnostic_logger.AzureDiagnosticLogging.info(MESSAGE1, "4200")
247+
assert diagnostic_logger.AzureDiagnosticLogging._initialized is True
248+
249+
# Verify instance methods work (if they exist)
250+
# Since this is primarily a class-based API, we just verify the singleton behavior

0 commit comments

Comments
 (0)