diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a9a994a..c60b6195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Added Status Logger + ([#217](https://github.com/microsoft/ApplicationInsights-Python/pull/217)) - Added Diagnostic Logging for App Service ([#212](https://github.com/microsoft/ApplicationInsights-Python/pull/212)) - Updated main and distro READMEs diff --git a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostics/__init__.py b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostic_logging.py b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostics/_diagnostic_logging.py similarity index 97% rename from azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostic_logging.py rename to azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostics/_diagnostic_logging.py index 1796f7bd..e0f3ad4c 100644 --- a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostic_logging.py +++ b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostics/_diagnostic_logging.py @@ -27,9 +27,6 @@ if _SUBSCRIPTION_ID_ENV_VAR else None ) -_opentelemetry_logger = logging.getLogger( - _OPENTELEMETRY_DIAGNOSTIC_LOGGER_NAME -) _logger = logging.getLogger(__name__) _DIAGNOSTIC_LOG_PATH = _get_log_path() diff --git a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostics/_status_logger.py b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostics/_status_logger.py new file mode 100644 index 00000000..574ec64f --- /dev/null +++ b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/_diagnostics/_status_logger.py @@ -0,0 +1,54 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from json import dumps +from os import getpid, makedirs +from os.path import exists, join +from platform import node + +from azure.monitor.opentelemetry.distro._constants import ( + _CUSTOMER_IKEY, + _EXTENSION_VERSION, + _IS_DIAGNOSTICS_ENABLED, + _get_log_path, +) +from azure.monitor.opentelemetry.distro._version import VERSION + +_MACHINE_NAME = node() +_STATUS_LOG_PATH = _get_log_path(status_log_path=True) + + +class AzureStatusLogger: + def _get_status_json(agent_initialized_successfully, pid, reason=None): + status_json = { + "AgentInitializedSuccessfully": agent_initialized_successfully, + "AppType": "python", + "MachineName": _MACHINE_NAME, + "PID": pid, + "SdkVersion": VERSION, + "Ikey": _CUSTOMER_IKEY, + "ExtensionVersion": _EXTENSION_VERSION, + } + if reason: + status_json["Reason"] = reason + return status_json + + def log_status(agent_initialized_successfully, reason=None): + if _IS_DIAGNOSTICS_ENABLED and _STATUS_LOG_PATH: + pid = getpid() + status_json = AzureStatusLogger._get_status_json( + agent_initialized_successfully, pid, reason + ) + if not exists(_STATUS_LOG_PATH): + makedirs(_STATUS_LOG_PATH) + # Change to be hostname and pid + status_logger_file_name = f"status_{_MACHINE_NAME}_{pid}.json" + with open( + join(_STATUS_LOG_PATH, status_logger_file_name), "w" + ) as f: + f.seek(0) + f.write(dumps(status_json)) + f.truncate() diff --git a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/configurator.py b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/configurator.py index 4445149b..3593471c 100644 --- a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/configurator.py +++ b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/configurator.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------- -from azure.monitor.opentelemetry.distro._diagnostic_logging import ( +from azure.monitor.opentelemetry.distro._diagnostics._diagnostic_logging import ( AzureDiagnosticLogging, ) from opentelemetry.sdk._configuration import _OTelSDKConfigurator diff --git a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/distro.py b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/distro.py index 703bb361..3b5b84ea 100644 --- a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/distro.py +++ b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/distro.py @@ -6,9 +6,12 @@ import logging from os import environ -from azure.monitor.opentelemetry.distro._diagnostic_logging import ( +from azure.monitor.opentelemetry.distro._diagnostics._diagnostic_logging import ( AzureDiagnosticLogging, ) +from azure.monitor.opentelemetry.distro._diagnostics._status_logger import ( + AzureStatusLogger, +) from opentelemetry.environment_variables import ( OTEL_METRICS_EXPORTER, OTEL_TRACES_EXPORTER, @@ -34,6 +37,7 @@ def _configure(self, **kwargs) -> None: def _configure_auto_instrumentation() -> None: try: + AzureStatusLogger.log_status(False, "Distro being configured.") AzureDiagnosticLogging.enable(_logger) AzureDiagnosticLogging.enable(_opentelemetry_logger) # TODO: Enabled when duplicate logging issue is solved @@ -51,10 +55,12 @@ def _configure_auto_instrumentation() -> None: environ.setdefault( OTEL_TRACES_EXPORTER, "azure_monitor_opentelemetry_exporter" ) + AzureStatusLogger.log_status(True) _logger.info( "Azure Monitor OpenTelemetry Distro configured successfully." ) except Exception as exc: + AzureStatusLogger.log_status(False, reason=exc) _logger.error( "Azure Monitor OpenTelemetry Distro failed during " + f"configuration: {exc}" diff --git a/azure-monitor-opentelemetry-distro/tests/test_diagnostic_logging.py b/azure-monitor-opentelemetry-distro/tests/diagnostics/test_diagnostic_logging.py similarity index 90% rename from azure-monitor-opentelemetry-distro/tests/test_diagnostic_logging.py rename to azure-monitor-opentelemetry-distro/tests/diagnostics/test_diagnostic_logging.py index 9a9beb15..d231d07c 100644 --- a/azure-monitor-opentelemetry-distro/tests/test_diagnostic_logging.py +++ b/azure-monitor-opentelemetry-distro/tests/diagnostics/test_diagnostic_logging.py @@ -12,7 +12,7 @@ from unittest import TestCase from unittest.mock import patch -import azure.monitor.opentelemetry.distro._diagnostic_logging as diagnostic_logger +import azure.monitor.opentelemetry.distro._diagnostics._diagnostic_logging as diagnostic_logger TEST_LOGGER_PATH = str(Path.home()) TEST_DIAGNOSTIC_LOGGER_FILE_NAME = "test-applicationinsights-extension.log" @@ -88,27 +88,27 @@ def set_up( reload(diagnostic_logger) assert not diagnostic_logger.AzureDiagnosticLogging._initialized patch( - "azure.monitor.opentelemetry.distro._diagnostic_logging._DIAGNOSTIC_LOG_PATH", + "azure.monitor.opentelemetry.distro._diagnostics._diagnostic_logging._DIAGNOSTIC_LOG_PATH", TEST_LOGGER_PATH, ).start() patch( - "azure.monitor.opentelemetry.distro._diagnostic_logging._DIAGNOSTIC_LOGGER_FILE_NAME", + "azure.monitor.opentelemetry.distro._diagnostics._diagnostic_logging._DIAGNOSTIC_LOGGER_FILE_NAME", TEST_DIAGNOSTIC_LOGGER_FILE_NAME, ).start() patch( - "azure.monitor.opentelemetry.distro._diagnostic_logging._CUSTOMER_IKEY", + "azure.monitor.opentelemetry.distro._diagnostics._diagnostic_logging._CUSTOMER_IKEY", TEST_CUSTOMER_IKEY, ).start() patch( - "azure.monitor.opentelemetry.distro._diagnostic_logging._EXTENSION_VERSION", + "azure.monitor.opentelemetry.distro._diagnostics._diagnostic_logging._EXTENSION_VERSION", TEST_EXTENSION_VERSION, ).start() patch( - "azure.monitor.opentelemetry.distro._diagnostic_logging.VERSION", + "azure.monitor.opentelemetry.distro._diagnostics._diagnostic_logging.VERSION", TEST_VERSION, ).start() patch( - "azure.monitor.opentelemetry.distro._diagnostic_logging._IS_DIAGNOSTICS_ENABLED", + "azure.monitor.opentelemetry.distro._diagnostics._diagnostic_logging._IS_DIAGNOSTICS_ENABLED", is_diagnostics_enabled, ).start() diagnostic_logger.AzureDiagnosticLogging.enable(logger) diff --git a/azure-monitor-opentelemetry-distro/tests/diagnostics/test_status_logger.py b/azure-monitor-opentelemetry-distro/tests/diagnostics/test_status_logger.py new file mode 100644 index 00000000..0df79142 --- /dev/null +++ b/azure-monitor-opentelemetry-distro/tests/diagnostics/test_status_logger.py @@ -0,0 +1,267 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from json import loads +from os.path import join +from pathlib import Path +from unittest import TestCase +from unittest.mock import patch + +from azure.monitor.opentelemetry.distro._diagnostics._status_logger import ( + AzureStatusLogger, +) + +TEST_LOGGER_PATH = str(Path.home()) +TEST_MACHINE_NAME = "TEST_MACHINE_NAME" +TEST_PID = 321 +TEST_STATUS_LOGGER_LOCATION = join( + TEST_LOGGER_PATH, f"status_{TEST_MACHINE_NAME}_{TEST_PID}.json" +) +TEST_OPERATION = "TEST_OPERATION" +TEST_OPERATION = "TEST_OPERATION" +TEST_SITE_NAME = "TEST_SITE_NAME" +TEST_CUSTOMER_IKEY = "TEST_CUSTOMER_IKEY" +TEST_EXTENSION_VERSION = "TEST_EXTENSION_VERSION" +TEST_VERSION = "TEST_VERSION" +TEST_SUBSCRIPTION_ID = "TEST_SUBSCRIPTION_ID" +MESSAGE1 = "MESSAGE1" +MESSAGE2 = "MESSAGE2" + + +def clear_file(): + with open(TEST_STATUS_LOGGER_LOCATION, "w") as f: + f.seek(0) + f.truncate() + + +def check_file_for_messages(agent_initialized_successfully, reason=None): + with open(TEST_STATUS_LOGGER_LOCATION, "r") as f: + f.seek(0) + json = loads(f.readline()) + assert ( + json["AgentInitializedSuccessfully"] + == agent_initialized_successfully + ) + assert json["AppType"] == "python" + assert json["MachineName"] == TEST_MACHINE_NAME + assert json["PID"] == TEST_PID + assert json["SdkVersion"] == TEST_VERSION + assert json["Ikey"] == TEST_CUSTOMER_IKEY + assert json["ExtensionVersion"] == TEST_EXTENSION_VERSION + if reason: + assert json["Reason"] == reason + else: + assert "Reason" not in json + assert not f.read() + + +def check_file_is_empty(): + with open(TEST_STATUS_LOGGER_LOCATION, "r") as f: + f.seek(0) + assert not f.read() + + +class TestStatusLogger(TestCase): + def setUp(self) -> None: + clear_file() + + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._CUSTOMER_IKEY", + TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + True, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_log_status_success(self, mock_getpid): + AzureStatusLogger.log_status(False, MESSAGE1) + AzureStatusLogger.log_status(True, MESSAGE2) + check_file_for_messages(True, MESSAGE2) + + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._CUSTOMER_IKEY", + TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + True, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_log_status_failed_initialization(self, mock_getpid): + AzureStatusLogger.log_status(True, MESSAGE1) + AzureStatusLogger.log_status(False, MESSAGE2) + check_file_for_messages(False, MESSAGE2) + + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._CUSTOMER_IKEY", + TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + True, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_log_status_no_reason(self, mock_getpid): + AzureStatusLogger.log_status(False, MESSAGE1) + AzureStatusLogger.log_status(True) + check_file_for_messages(True) + + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._CUSTOMER_IKEY", + TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + False, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_disabled_log_status_success(self, mock_getpid): + AzureStatusLogger.log_status(False, MESSAGE1) + AzureStatusLogger.log_status(True, MESSAGE2) + check_file_is_empty() + + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._CUSTOMER_IKEY", + TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + False, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_disabled_log_status_failed_initialization(self, mock_getpid): + AzureStatusLogger.log_status(True, MESSAGE1) + AzureStatusLogger.log_status(False, MESSAGE2) + check_file_is_empty() + + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._STATUS_LOG_PATH", + TEST_LOGGER_PATH, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._CUSTOMER_IKEY", + TEST_CUSTOMER_IKEY, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._EXTENSION_VERSION", + TEST_EXTENSION_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.VERSION", + TEST_VERSION, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._IS_DIAGNOSTICS_ENABLED", + False, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger.getpid", + return_value=TEST_PID, + ) + @patch( + "azure.monitor.opentelemetry.distro._diagnostics._status_logger._MACHINE_NAME", + TEST_MACHINE_NAME, + ) + def test_disabled_log_status_no_reason(self, mock_getpid): + AzureStatusLogger.log_status(False, MESSAGE1) + AzureStatusLogger.log_status(True) + check_file_is_empty()