def test_health_status_single_provider(pyctuator_impl: PyctuatorImpl) -> None: health_provider = MyHealthProvider() pyctuator_impl.register_health_providers(health_provider) # Test's default status is UNKNOWN assert pyctuator_impl.get_health().status == Status.UNKNOWN health_provider.down() assert pyctuator_impl.get_health().status == Status.DOWN health_provider.up() assert pyctuator_impl.get_health().status == Status.UP
def test_health_status_multiple_providers( pyctuator_impl: PyctuatorImpl) -> None: health_providers = [ MyHealthProvider("kuki"), MyHealthProvider("puki"), MyHealthProvider("ruki") ] for health_provider in health_providers: pyctuator_impl.register_health_providers(health_provider) # Test's default status is UNKNOWN - all 3 are UNKNOWN assert pyctuator_impl.get_health().status == Status.UNKNOWN health_providers[0].down() assert pyctuator_impl.get_health().status == Status.DOWN health_providers[0].up() assert pyctuator_impl.get_health().status == Status.UP # first provider is UP, but the second is DOWN health_providers[1].down() assert pyctuator_impl.get_health().status == Status.DOWN # first and second providers are UP, 3rd is UNKNOWN health_providers[1].up() assert pyctuator_impl.get_health().status == Status.UP
def __init__( self, app: Any, app_name: str, app_url: str, pyctuator_endpoint_url: str, registration_url: Optional[str], registration_auth: Optional[Auth] = None, app_description: Optional[str] = None, registration_interval_sec: int = 10, free_disk_space_down_threshold_bytes: int = 1024 * 1024 * 100, logfile_max_size: int = 10000, logfile_formatter: str = default_logfile_format, auto_deregister: bool = True, ) -> None: """The entry point for integrating pyctuator with a web-frameworks such as FastAPI and Flask. Given an application built on top of a supported web-framework, it'll add to the application the REST API endpoints that required for Spring Boot Admin to monitor and manage the application. Pyctuator currently supports application built on top of FastAPI and Flask. The type of first argument, app is specific to the target web-framework: * FastAPI - `app` is an instance of `fastapi.applications.FastAPI` * Flask - `app` is an instance of `flask.app.Flask` * aiohttp - `app`is an instance of `aiohttp.web.Application` :param app: an instance of a supported web-framework with which the pyctuator endpoints will be registered :param app_name: the application's name that will be presented in the "Info" section in boot-admin :param app_description: a description that will be presented in the "Info" section in boot-admin :param app_url: the full URL of the application being monitored which will be displayed in spring-boot-admin, we recommend this URL to be accessible by those who manage the application (i.e. don't use "http://localhost..." as it is only accessible from within the application's host) :param pyctuator_endpoint_url: the public URL from which Pyctuator REST API will be accessible, used for registering the application with spring-boot-admin, must be accessible from spring-boot-admin server (i.e. don't use http://localhost:8080/... unless spring-boot-admin is running on the same host as the monitored application) :param registration_url: the spring-boot-admin endpoint to which registration requests must be posted :param registration_auth: optional authentication details to use when registering with spring-boot-admin :param registration_interval_sec: how often pyctuator will renew its registration with spring-boot-admin :param free_disk_space_down_threshold_bytes: amount of free space in bytes in "./" (the application's current working directory) below which the built-in disk-space health-indicator will fail """ self.auto_deregister = auto_deregister start_time = datetime.now(timezone.utc) # Instantiate an instance of PyctuatorImpl which abstracts the state and logic of the pyctuator self.pyctuator_impl = PyctuatorImpl( AppInfo(app=AppDetails(name=app_name, description=app_description)), pyctuator_endpoint_url, logfile_max_size, logfile_formatter, ) # Register default health/metrics/environment providers self.pyctuator_impl.register_environment_provider(OsEnvironmentVariableProvider()) self.pyctuator_impl.register_health_providers(DiskSpaceHealthProvider(free_disk_space_down_threshold_bytes)) self.pyctuator_impl.register_metrics_provider(MemoryMetricsProvider()) self.pyctuator_impl.register_metrics_provider(ThreadMetricsProvider()) self.boot_admin_registration_handler: Optional[BootAdminRegistrationHandler] = None root_logger = logging.getLogger() root_logger.addHandler(self.pyctuator_impl.logfile.log_messages) # Find and initialize an integration layer between the web-framework adn pyctuator framework_integrations = { "flask": self._integrate_flask, "fastapi": self._integrate_fastapi, "aiohttp": self._integrate_aiohttp, } for framework_name, framework_integration_function in framework_integrations.items(): if self._is_framework_installed(framework_name): logging.debug("Framework %s is installed, trying to integrate with it", framework_name) success = framework_integration_function(app, self.pyctuator_impl) if success: logging.debug("Integrated with framework %s", framework_name) if registration_url is not None: self.boot_admin_registration_handler = BootAdminRegistrationHandler( registration_url, registration_auth, app_name, self.pyctuator_impl.pyctuator_endpoint_url, start_time, app_url, registration_interval_sec, ) # Deregister from SBA on exit if self.auto_deregister: atexit.register(self.boot_admin_registration_handler.deregister_from_admin_server) self.boot_admin_registration_handler.start() return # Fail in case no framework was found for the target app raise EnvironmentError("No framework was found that is matching the target app" "(is it properly installed and imported?)")
class Pyctuator: # pylint: disable=too-many-locals def __init__( self, app: Any, app_name: str, app_url: str, pyctuator_endpoint_url: str, registration_url: Optional[str], registration_auth: Optional[Auth] = None, app_description: Optional[str] = None, registration_interval_sec: int = 10, free_disk_space_down_threshold_bytes: int = 1024 * 1024 * 100, logfile_max_size: int = 10000, logfile_formatter: str = default_logfile_format, auto_deregister: bool = True, ) -> None: """The entry point for integrating pyctuator with a web-frameworks such as FastAPI and Flask. Given an application built on top of a supported web-framework, it'll add to the application the REST API endpoints that required for Spring Boot Admin to monitor and manage the application. Pyctuator currently supports application built on top of FastAPI and Flask. The type of first argument, app is specific to the target web-framework: * FastAPI - `app` is an instance of `fastapi.applications.FastAPI` * Flask - `app` is an instance of `flask.app.Flask` * aiohttp - `app` is an instance of `aiohttp.web.Application` * Tornado - `app` is an instance of `tornado.web.Application` :param app: an instance of a supported web-framework with which the pyctuator endpoints will be registered :param app_name: the application's name that will be presented in the "Info" section in boot-admin :param app_description: a description that will be presented in the "Info" section in boot-admin :param app_url: the full URL of the application being monitored which will be displayed in spring-boot-admin, we recommend this URL to be accessible by those who manage the application (i.e. don't use "http://localhost..." as it is only accessible from within the application's host) :param pyctuator_endpoint_url: the public URL from which Pyctuator REST API will be accessible, used for registering the application with spring-boot-admin, must be accessible from spring-boot-admin server (i.e. don't use http://localhost:8080/... unless spring-boot-admin is running on the same host as the monitored application) :param registration_url: the spring-boot-admin endpoint to which registration requests must be posted :param registration_auth: optional authentication details to use when registering with spring-boot-admin :param registration_interval_sec: how often pyctuator will renew its registration with spring-boot-admin :param free_disk_space_down_threshold_bytes: amount of free space in bytes in "./" (the application's current working directory) below which the built-in disk-space health-indicator will fail """ self.auto_deregister = auto_deregister start_time = datetime.now(timezone.utc) # Instantiate an instance of PyctuatorImpl which abstracts the state and logic of the pyctuator self.pyctuator_impl = PyctuatorImpl( AppInfo( app=AppDetails(name=app_name, description=app_description)), pyctuator_endpoint_url, logfile_max_size, logfile_formatter, ) # Register default health/metrics/environment providers self.pyctuator_impl.register_environment_provider( OsEnvironmentVariableProvider()) self.pyctuator_impl.register_health_providers( DiskSpaceHealthProvider(free_disk_space_down_threshold_bytes)) self.pyctuator_impl.register_metrics_provider(MemoryMetricsProvider()) self.pyctuator_impl.register_metrics_provider(ThreadMetricsProvider()) self.boot_admin_registration_handler: Optional[ BootAdminRegistrationHandler] = None root_logger = logging.getLogger() # If application did not initiate logging module, add default handler to root logger # logging.info implicitly calls logging.basicConfig(), see logging.basicConfig in Python's documentation. if not root_logger.hasHandlers(): logging.info("Logging not configured, using logging.basicConfig()") root_logger.addHandler(self.pyctuator_impl.logfile.log_messages) # Find and initialize an integration layer between the web-framework adn pyctuator framework_integrations = { "flask": self._integrate_flask, "fastapi": self._integrate_fastapi, "aiohttp": self._integrate_aiohttp, "tornado": self._integrate_tornado } for framework_name, framework_integration_function in framework_integrations.items( ): if self._is_framework_installed(framework_name): logging.debug( "Framework %s is installed, trying to integrate with it", framework_name) success = framework_integration_function( app, self.pyctuator_impl) if success: logging.debug("Integrated with framework %s", framework_name) if registration_url is not None: self.boot_admin_registration_handler = BootAdminRegistrationHandler( registration_url, registration_auth, app_name, self.pyctuator_impl.pyctuator_endpoint_url, start_time, app_url, registration_interval_sec, ) # Deregister from SBA on exit if self.auto_deregister: atexit.register( self.boot_admin_registration_handler. deregister_from_admin_server) self.boot_admin_registration_handler.start() return # Fail in case no framework was found for the target app raise EnvironmentError( "No framework was found that is matching the target app " "(is it properly installed and imported?)") def stop(self) -> None: if self.boot_admin_registration_handler: self.boot_admin_registration_handler.stop() self.boot_admin_registration_handler = None def register_environment_provider( self, name: str, env_provider: Callable[[], Dict]) -> None: self.pyctuator_impl.register_environment_provider( CustomEnvironmentProvider(name, env_provider)) def register_health_provider(self, provider: HealthProvider) -> None: self.pyctuator_impl.register_health_providers(provider) def set_git_info(self, commit: str, time: datetime, branch: Optional[str] = None) -> None: self.pyctuator_impl.set_git_info( GitInfo(GitCommitInfo(time, commit), branch)) def set_build_info( self, artifact: Optional[str] = None, group: Optional[str] = None, name: Optional[str] = None, version: Optional[str] = None, time: Optional[datetime] = None, ) -> None: self.pyctuator_impl.set_build_info( BuildInfo(name, artifact, group, version, time)) def _is_framework_installed(self, framework_name: str) -> bool: return importlib.util.find_spec(framework_name) is not None def _integrate_fastapi(self, app: Any, pyctuator_impl: PyctuatorImpl) -> bool: """ This method should only be called if we detected that FastAPI is installed. It will then check whether the given app is a FastAPI app, and if so - it will add the Pyctuator endpoints to it. """ from fastapi import FastAPI if isinstance(app, FastAPI): from pyctuator.impl.fastapi_pyctuator import FastApiPyctuator FastApiPyctuator(app, pyctuator_impl, False) return True return False def _integrate_flask(self, app: Any, pyctuator_impl: PyctuatorImpl) -> bool: """ This method should only be called if we detected that Flask is installed. It will then check whether the given app is a Flask app, and if so - it will add the Pyctuator endpoints to it. """ from flask import Flask if isinstance(app, Flask): from pyctuator.impl.flask_pyctuator import FlaskPyctuator FlaskPyctuator(app, pyctuator_impl) return True return False def _integrate_aiohttp(self, app: Any, pyctuator_impl: PyctuatorImpl) -> bool: """ This method should only be called if we detected that aiohttp is installed. It will then check whether the given app is a aiohttp app, and if so - it will add the Pyctuator endpoints to it. """ from aiohttp.web import Application if isinstance(app, Application): from pyctuator.impl.aiohttp_pyctuator import AioHttpPyctuator AioHttpPyctuator(app, pyctuator_impl) return True return False def _integrate_tornado(self, app: Any, pyctuator_impl: PyctuatorImpl) -> bool: """ This method should only be called if we detected that tornado is installed. It will then check whether the given app is a tornado app, and if so - it will add the Pyctuator endpoints to it. """ from tornado.web import Application if isinstance(app, Application): from pyctuator.impl.tornado_pyctuator import TornadoHttpPyctuator TornadoHttpPyctuator(app, pyctuator_impl) return True return False
def pyctuator_impl() -> PyctuatorImpl: return PyctuatorImpl(AppInfo(app=AppDetails(name="appy")), "http://appy/pyctuator", 10, default_logfile_format)