diff --git a/src/instana/collector/helpers/runtime.py b/src/instana/collector/helpers/runtime.py index 8156ffd9..8b33cb5c 100644 --- a/src/instana/collector/helpers/runtime.py +++ b/src/instana/collector/helpers/runtime.py @@ -10,7 +10,7 @@ import sys import threading from types import ModuleType -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Optional, Union from instana.collector.base import BaseCollector from instana.collector.helpers.base import BaseHelper @@ -30,7 +30,7 @@ def is_autowrapt_instrumented() -> bool: def is_webhook_instrumented() -> bool: - return any(map(lambda p: PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR in p, sys.path)) + return any(PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR in p for p in sys.path) class RuntimeHelper(BaseHelper): @@ -44,13 +44,11 @@ def __init__( self.previous = DictionaryOfStan() self.previous_rusage = get_resource_usage() - if gc.isenabled(): - self.previous_gc_count = gc.get_count() - else: - self.previous_gc_count = None + gc_enabled = gc.isenabled() + self.previous_gc_count = gc.get_count() if gc_enabled else None - def collect_metrics(self, **kwargs: Dict[str, Any]) -> List[Dict[str, Any]]: - plugin_data = dict() + def collect_metrics(self, **kwargs: Any) -> list[dict[str, Any]]: + plugin_data = {} try: plugin_data["name"] = "com.instana.plugin.python" plugin_data["entityId"] = str(os.getpid()) @@ -75,253 +73,132 @@ def collect_metrics(self, **kwargs: Dict[str, Any]) -> List[Dict[str, Any]]: def _collect_runtime_metrics( self, - plugin_data: Dict[str, Any], + plugin_data: dict[str, Any], with_snapshot: bool, ) -> None: + """Collect and report runtime resource-usage metrics.""" if os.environ.get("INSTANA_DISABLE_METRICS_COLLECTION", False): return - """ Collect up and return the runtime metrics """ + rusage = get_resource_usage() + prev = self.previous_rusage try: - rusage = get_resource_usage() if gc.isenabled(): self._collect_gc_metrics(plugin_data, with_snapshot) self._collect_thread_metrics(plugin_data, with_snapshot) - value_diff = rusage.ru_utime - self.previous_rusage.ru_utime + pm = self.previous["data"]["metrics"] + nm = plugin_data["data"]["metrics"] + # Delta (counter) fields — direct attribute access avoids getattr overhead self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_utime", - with_snapshot, + rusage.ru_utime - prev.ru_utime, pm, nm, "ru_utime", with_snapshot ) - - value_diff = rusage.ru_stime - self.previous_rusage.ru_stime self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_stime", - with_snapshot, + rusage.ru_stime - prev.ru_stime, pm, nm, "ru_stime", with_snapshot ) - self.apply_delta( - rusage.ru_maxrss, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_maxrss", - with_snapshot, + rusage.ru_minflt - prev.ru_minflt, pm, nm, "ru_minflt", with_snapshot ) self.apply_delta( - rusage.ru_ixrss, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_ixrss", - with_snapshot, + rusage.ru_majflt - prev.ru_majflt, pm, nm, "ru_majflt", with_snapshot ) self.apply_delta( - rusage.ru_idrss, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_idrss", - with_snapshot, + rusage.ru_nswap - prev.ru_nswap, pm, nm, "ru_nswap", with_snapshot ) self.apply_delta( - rusage.ru_isrss, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_isrss", - with_snapshot, + rusage.ru_inblock - prev.ru_inblock, pm, nm, "ru_inblock", with_snapshot ) - - value_diff = rusage.ru_minflt - self.previous_rusage.ru_minflt self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_minflt", - with_snapshot, + rusage.ru_oublock - prev.ru_oublock, pm, nm, "ru_oublock", with_snapshot ) - - value_diff = rusage.ru_majflt - self.previous_rusage.ru_majflt self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_majflt", - with_snapshot, + rusage.ru_msgsnd - prev.ru_msgsnd, pm, nm, "ru_msgsnd", with_snapshot ) - - value_diff = rusage.ru_nswap - self.previous_rusage.ru_nswap self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_nswap", - with_snapshot, + rusage.ru_msgrcv - prev.ru_msgrcv, pm, nm, "ru_msgrcv", with_snapshot ) - - value_diff = rusage.ru_inblock - self.previous_rusage.ru_inblock self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_inblock", - with_snapshot, - ) - - value_diff = rusage.ru_oublock - self.previous_rusage.ru_oublock - self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_oublock", - with_snapshot, - ) - - value_diff = rusage.ru_msgsnd - self.previous_rusage.ru_msgsnd - self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_msgsnd", - with_snapshot, - ) - - value_diff = rusage.ru_msgrcv - self.previous_rusage.ru_msgrcv - self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_msgrcv", - with_snapshot, - ) - - value_diff = rusage.ru_nsignals - self.previous_rusage.ru_nsignals - self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], + rusage.ru_nsignals - prev.ru_nsignals, + pm, + nm, "ru_nsignals", with_snapshot, ) - - value_diff = rusage.ru_nvcsw - self.previous_rusage.ru_nvcsw self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_nvcsw", - with_snapshot, + rusage.ru_nvcsw - prev.ru_nvcsw, pm, nm, "ru_nvcsw", with_snapshot ) - - value_diff = rusage.ru_nivcsw - self.previous_rusage.ru_nivcsw self.apply_delta( - value_diff, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "ru_nivcsw", - with_snapshot, + rusage.ru_nivcsw - prev.ru_nivcsw, pm, nm, "ru_nivcsw", with_snapshot ) + # Absolute (gauge) fields + self.apply_delta(rusage.ru_maxrss, pm, nm, "ru_maxrss", with_snapshot) + self.apply_delta(rusage.ru_ixrss, pm, nm, "ru_ixrss", with_snapshot) + self.apply_delta(rusage.ru_idrss, pm, nm, "ru_idrss", with_snapshot) + self.apply_delta(rusage.ru_isrss, pm, nm, "ru_isrss", with_snapshot) except Exception: logger.debug("_collect_runtime_metrics", exc_info=True) finally: self.previous_rusage = rusage - def _collect_gc_metrics(self, plugin_data, with_snapshot): + def _collect_gc_metrics( + self, + plugin_data: dict[str, Any], + with_snapshot: bool, + ) -> None: try: gc_count = gc.get_count() gc_threshold = gc.get_threshold() - self.apply_delta( - gc_count[0], - self.previous["data"]["metrics"]["gc"], - plugin_data["data"]["metrics"]["gc"], - "collect0", - with_snapshot, - ) - self.apply_delta( - gc_count[1], - self.previous["data"]["metrics"]["gc"], - plugin_data["data"]["metrics"]["gc"], - "collect1", - with_snapshot, - ) - self.apply_delta( - gc_count[2], - self.previous["data"]["metrics"]["gc"], - plugin_data["data"]["metrics"]["gc"], - "collect2", - with_snapshot, - ) - - self.apply_delta( - gc_threshold[0], - self.previous["data"]["metrics"]["gc"], - plugin_data["data"]["metrics"]["gc"], - "threshold0", - with_snapshot, - ) - self.apply_delta( - gc_threshold[1], - self.previous["data"]["metrics"]["gc"], - plugin_data["data"]["metrics"]["gc"], - "threshold1", - with_snapshot, - ) - self.apply_delta( - gc_threshold[2], - self.previous["data"]["metrics"]["gc"], - plugin_data["data"]["metrics"]["gc"], - "threshold2", - with_snapshot, - ) + gc_metrics = { + "collect0": gc_count[0], + "collect1": gc_count[1], + "collect2": gc_count[2], + "threshold0": gc_threshold[0], + "threshold1": gc_threshold[1], + "threshold2": gc_threshold[2], + } + prev_gc = self.previous["data"]["metrics"]["gc"] + new_gc = plugin_data["data"]["metrics"]["gc"] + for metric, value in gc_metrics.items(): + self.apply_delta(value, prev_gc, new_gc, metric, with_snapshot) except Exception: logger.debug("_collect_gc_metrics", exc_info=True) def _collect_thread_metrics( self, - plugin_data: Dict[str, Any], + plugin_data: dict[str, Any], with_snapshot: bool, ) -> None: try: threads = threading.enumerate() - daemon_threads = [thread.daemon is True for thread in threads].count(True) + # Single pass: avoids three separate list-comprehensions and a + # temporary dict, which is the fastest approach for small lists. + daemon = alive = dummy = 0 + for t in threads: + if isinstance(t, threading._DummyThread): # pylint: disable=protected-access + dummy += 1 + elif t.daemon: + daemon += 1 + else: + alive += 1 + prev_metrics = self.previous["data"]["metrics"] + new_metrics = plugin_data["data"]["metrics"] self.apply_delta( - daemon_threads, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "daemon_threads", - with_snapshot, + daemon, prev_metrics, new_metrics, "daemon_threads", with_snapshot ) - - alive_threads = [thread.daemon is False for thread in threads].count(True) self.apply_delta( - alive_threads, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "alive_threads", - with_snapshot, + alive, prev_metrics, new_metrics, "alive_threads", with_snapshot ) - - dummy_threads = [ - isinstance(thread, threading._DummyThread) for thread in threads - ].count(True) # pylint: disable=protected-access self.apply_delta( - dummy_threads, - self.previous["data"]["metrics"], - plugin_data["data"]["metrics"], - "dummy_threads", - with_snapshot, + dummy, prev_metrics, new_metrics, "dummy_threads", with_snapshot ) except Exception: logger.debug("_collect_thread_metrics", exc_info=True) def _collect_runtime_snapshot( self, - plugin_data: Dict[str, Any], + plugin_data: dict[str, Any], ) -> None: """Gathers Python specific Snapshot information for this process""" snapshot_payload = {} @@ -353,14 +230,45 @@ def _collect_runtime_snapshot( ): snapshot_payload["djmw"] = settings.MIDDLEWARE_CLASSES except Exception: - pass + logger.debug( + "_collect_runtime_snapshot: django settings unavailable", + exc_info=True, + ) except Exception: logger.debug("collect_snapshot: ", exc_info=True) plugin_data["data"]["snapshot"] = snapshot_payload - def gather_python_packages(self) -> Dict[str, Any]: - """Collect up the list of modules in use""" + def _resolve_package_version( + self, pkg_name: str, module: ModuleType + ) -> Optional[str]: + """Return the version string for a single module, or None if undiscoverable. + + Args: + pkg_name: The top-level package name (e.g. "django"). + module: The module object from sys.modules. + + Returns: + A version string, or None when the version cannot be determined. + """ + try: + pkg_info = module.__dict__ + if "__version__" in pkg_info: + v = pkg_info["__version__"] + return v if isinstance(v, str) else self.jsonable(v) + if "version" in pkg_info: + return self.jsonable(pkg_info["version"]) + return importlib.metadata.version(pkg_name) + except importlib.metadata.PackageNotFoundError: + return None + except Exception: + logger.debug( + f"gather_python_packages: could not process module: {pkg_name}", + ) + return None + + def gather_python_packages(self) -> dict[str, str]: + """Collect up the list of modules in use.""" if os.environ.get("INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION"): return {"instana": VERSION} @@ -368,36 +276,20 @@ def gather_python_packages(self) -> Dict[str, Any]: try: sys_packages = sys.modules.copy() - for pkg_name in sys_packages: + for pkg_name, module in sys_packages.items(): # Don't report submodules (e.g. django.x, django.y, django.z) # Skip modules that begin with underscore - if ("." in pkg_name) or pkg_name[0] == "_": + if ("." in pkg_name) or pkg_name.startswith("_"): continue # Skip builtins if pkg_name in ["sys", "curses"]: continue - if sys_packages[pkg_name]: - try: - pkg_info = sys_packages[pkg_name].__dict__ - if "__version__" in pkg_info: - if isinstance(pkg_info["__version__"], str): - versions[pkg_name] = pkg_info["__version__"] - else: - versions[pkg_name] = self.jsonable( - pkg_info["__version__"] - ) - elif "version" in pkg_info: - versions[pkg_name] = self.jsonable(pkg_info["version"]) - else: - versions[pkg_name] = importlib.metadata.version(pkg_name) - except importlib.metadata.PackageNotFoundError: - pass - except Exception: - logger.debug( - f"gather_python_packages: could not process module: {pkg_name}", - ) + if module: + version = self._resolve_package_version(pkg_name, module) + if version is not None: + versions[pkg_name] = version # Manually set our package version versions["instana"] = VERSION @@ -408,7 +300,7 @@ def gather_python_packages(self) -> Dict[str, Any]: def jsonable( self, - value: Union[Callable[[], Any], ModuleType, Any], + value: Union[Callable[[], str], ModuleType, object], ) -> str: try: if callable(value): @@ -423,3 +315,4 @@ def jsonable( return str(result) except Exception: logger.debug("jsonable: ", exc_info=True) + return ""