Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ dependencies = [
"opentelemetry-semantic-conventions>=0.48b0",
"typing_extensions>=4.12.2",
"pyyaml>=6.0.2",
"setuptools>=69.0.0; python_version >= \"3.12\"",
"setuptools>=69.0.0; python_version >= \"3.12\"",
"psutil>=5.9.0; sys_platform == \"win32\"",
]

[project.entry-points."instana"]
Expand Down
146 changes: 146 additions & 0 deletions src/instana/collector/helpers/resource_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# (c) Copyright IBM Corp. 2025

"""Cross-platform resource usage information"""

import os
from typing import NamedTuple

from instana.log import logger
from instana.util.runtime import is_windows


class ResourceUsage(NamedTuple):
"""
Cross-platform resource usage information, mirroring fields found in the Unix rusage struct.

Attributes:
ru_utime (float): User CPU time used (seconds).
ru_stime (float): System CPU time used (seconds).
ru_maxrss (int): Maximum resident set size used (bytes).
ru_ixrss (int): Integral shared memory size (bytes).
ru_idrss (int): Integral unshared data size (bytes).
ru_isrss (int): Integral unshared stack size (bytes).
ru_minflt (int): Number of page reclaims (soft page faults).
ru_majflt (int): Number of page faults requiring I/O (hard page faults).
ru_nswap (int): Number of times a process was swapped out.
ru_inblock (int): Number of file system input blocks.
ru_oublock (int): Number of file system output blocks.
ru_msgsnd (int): Number of messages sent.
ru_msgrcv (int): Number of messages received.
ru_nsignals (int): Number of signals received.
ru_nvcsw (int): Number of voluntary context switches.
ru_nivcsw (int): Number of involuntary context switches.
"""

ru_utime: float = 0.0
ru_stime: float = 0.0
ru_maxrss: int = 0
ru_ixrss: int = 0
ru_idrss: int = 0
ru_isrss: int = 0
ru_minflt: int = 0
ru_majflt: int = 0
ru_nswap: int = 0
ru_inblock: int = 0
ru_oublock: int = 0
ru_msgsnd: int = 0
ru_msgrcv: int = 0
ru_nsignals: int = 0
ru_nvcsw: int = 0
ru_nivcsw: int = 0


def get_resource_usage() -> ResourceUsage:
"""Get resource usage in a cross-platform way"""
if is_windows():
return _get_windows_resource_usage()
else:
return _get_unix_resource_usage()


def _get_unix_resource_usage() -> ResourceUsage:
"""Get resource usage on Unix systems"""
import resource

rusage = resource.getrusage(resource.RUSAGE_SELF)

return ResourceUsage(
ru_utime=rusage.ru_utime,
ru_stime=rusage.ru_stime,
ru_maxrss=rusage.ru_maxrss,
ru_ixrss=rusage.ru_ixrss,
ru_idrss=rusage.ru_idrss,
ru_isrss=rusage.ru_isrss,
ru_minflt=rusage.ru_minflt,
ru_majflt=rusage.ru_majflt,
ru_nswap=rusage.ru_nswap,
ru_inblock=rusage.ru_inblock,
ru_oublock=rusage.ru_oublock,
ru_msgsnd=rusage.ru_msgsnd,
ru_msgrcv=rusage.ru_msgrcv,
ru_nsignals=rusage.ru_nsignals,
ru_nvcsw=rusage.ru_nvcsw,
ru_nivcsw=rusage.ru_nivcsw,
)


def _get_windows_resource_usage() -> ResourceUsage:
"""Get resource usage on Windows systems"""
# On Windows, we can use psutil to get some of the metrics
# For metrics that aren't available, we return 0
try:
import psutil

process = psutil.Process(os.getpid())

# Get CPU times
cpu_times = process.cpu_times()

# Get memory info
memory_info = process.memory_info()

# Get IO counters
io_counters = process.io_counters() if hasattr(process, "io_counters") else None

# Get context switch counts if available
ctx_switches = (
process.num_ctx_switches() if hasattr(process, "num_ctx_switches") else None
)

return ResourceUsage(
ru_utime=cpu_times.user if hasattr(cpu_times, "user") else 0.0,
ru_stime=cpu_times.system if hasattr(cpu_times, "system") else 0.0,
ru_maxrss=memory_info.rss // 1024
if hasattr(memory_info, "rss")
else 0, # Convert to KB to match Unix
ru_ixrss=0, # Not available on Windows
ru_idrss=0, # Not available on Windows
ru_isrss=0, # Not available on Windows
ru_minflt=0, # Not directly available on Windows
ru_majflt=0, # Not directly available on Windows
ru_nswap=0, # Not available on Windows
ru_inblock=io_counters.read_count
if io_counters and hasattr(io_counters, "read_count")
else 0,
ru_oublock=io_counters.write_count
if io_counters and hasattr(io_counters, "write_count")
else 0,
ru_msgsnd=0, # Not available on Windows
ru_msgrcv=0, # Not available on Windows
ru_nsignals=0, # Not available on Windows
ru_nvcsw=ctx_switches.voluntary
if ctx_switches and hasattr(ctx_switches, "voluntary")
else 0,
ru_nivcsw=ctx_switches.involuntary
if ctx_switches and hasattr(ctx_switches, "involuntary")
else 0,
)
except ImportError:
# If psutil is not available, return zeros
logger.debug(
"get_windows_resource_usage: psutil is not available, returning zeros"
)
return ResourceUsage()


# Made with Bob
10 changes: 5 additions & 5 deletions src/instana/collector/helpers/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
import importlib.metadata
import os
import platform
import resource
import sys
import threading
from types import ModuleType
from typing import Any, Dict, List, Union, Callable
from typing import Any, Callable, Dict, List, Union

from instana.collector.base import BaseCollector
from instana.collector.helpers.base import BaseHelper
from instana.collector.helpers.resource_usage import get_resource_usage
from instana.log import logger
from instana.util import DictionaryOfStan
from instana.util.runtime import determine_service_name
from instana.version import VERSION
from instana.collector.base import BaseCollector

PATH_OF_DEPRECATED_INSTALLATION_VIA_HOST_AGENT = "/tmp/.instana/python"

Expand All @@ -42,7 +42,7 @@ def __init__(
) -> None:
super(RuntimeHelper, self).__init__(collector)
self.previous = DictionaryOfStan()
self.previous_rusage = resource.getrusage(resource.RUSAGE_SELF)
self.previous_rusage = get_resource_usage()

if gc.isenabled():
self.previous_gc_count = gc.get_count()
Expand Down Expand Up @@ -83,7 +83,7 @@ def _collect_runtime_metrics(

""" Collect up and return the runtime metrics """
try:
rusage = resource.getrusage(resource.RUSAGE_SELF)
rusage = get_resource_usage()
if gc.isenabled():
self._collect_gc_metrics(plugin_data, with_snapshot)

Expand Down
93 changes: 66 additions & 27 deletions src/instana/util/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
def get_py_source(filename: str) -> Dict[str, str]:
"""
Retrieves the source code for Python files requested by the UI via the host agent.

This function reads and returns the content of Python source files. It validates
that the requested file has a .py extension and returns an appropriate error
message if the file cannot be read or is not a Python file.

Args:
filename (str): The fully qualified path to a Python source file

Returns:
Dict[str, str]: A dictionary containing either:
- {"data": source_code} if successful
Expand All @@ -32,7 +32,7 @@ def get_py_source(filename: str) -> Dict[str, str]:
response = {"error": "Only Python source files are allowed. (*.py)"}
else:
pysource = ""
with open(filename, 'r') as pyfile:
with open(filename, "r") as pyfile:
pysource = pyfile.read()

response = {"data": pysource}
Expand All @@ -50,7 +50,7 @@ def get_py_source(filename: str) -> Dict[str, str]:
def determine_service_name() -> str:
"""
Determines the most appropriate service name for this application process.

The service name is determined using the following priority order:
1. INSTANA_SERVICE_NAME environment variable if set
2. For specific frameworks:
Expand All @@ -61,7 +61,7 @@ def determine_service_name() -> str:
3. Command line arguments (first non-option argument)
4. Executable name
5. "python" as a fallback

Returns:
str: The determined service name
"""
Expand All @@ -75,13 +75,13 @@ def determine_service_name() -> str:
basename = None

try:
if not hasattr(sys, 'argv'):
if not hasattr(sys, "argv"):
proc_cmdline = get_proc_cmdline(as_string=False)
return os.path.basename(proc_cmdline[0])

# Get first argument that is not an CLI option
for candidate in sys.argv:
if len(candidate) > 0 and candidate[0] != '-':
if len(candidate) > 0 and candidate[0] != "-":
basename = candidate
break

Expand All @@ -93,7 +93,7 @@ def determine_service_name() -> str:
basename = os.path.basename(basename)

if basename == "gunicorn":
if 'setproctitle' in sys.modules:
if "setproctitle" in sys.modules:
# With the setproctitle package, gunicorn renames their processes
# to pretty things - we use those by default
# gunicorn: master [djface.wsgi]
Expand All @@ -104,8 +104,8 @@ def determine_service_name() -> str:
elif "FLASK_APP" in os.environ:
app_name = os.environ["FLASK_APP"]
elif "DJANGO_SETTINGS_MODULE" in os.environ:
app_name = os.environ["DJANGO_SETTINGS_MODULE"].split('.')[0]
elif basename == '':
app_name = os.environ["DJANGO_SETTINGS_MODULE"].split(".")[0]
elif basename == "":
if sys.stdout.isatty():
app_name = "Interactive Console"
else:
Expand Down Expand Up @@ -142,21 +142,22 @@ def determine_service_name() -> str:

return app_name


def get_proc_cmdline(as_string: bool = False) -> Union[List[str], str]:
"""
Parses the process command line from the proc file system.

This function attempts to read the command line of the current process from
/proc/self/cmdline. If the proc filesystem is not available (e.g., on non-Unix
systems), it returns a default value.

Args:
as_string (bool, optional): If True, returns the command line as a single
as_string (bool, optional): If True, returns the command line as a single
space-separated string. If False, returns a list
of command line arguments. Defaults to False.

Returns:
Union[List[str], str]: The command line as either a list of arguments or a
Union[List[str], str]: The command line as either a list of arguments or a
space-separated string, depending on the as_string parameter.
"""
name = "python"
Expand All @@ -172,7 +173,7 @@ def get_proc_cmdline(as_string: bool = False) -> Union[List[str], str]:

# /proc/self/command line will have strings with null bytes such as "/usr/bin/python\0-s\0-d\0". This
# bit will prep the return value and drop the trailing null byte
parts = name.split('\0')
parts = name.split("\0")
parts.pop()

if as_string is True:
Expand All @@ -181,32 +182,70 @@ def get_proc_cmdline(as_string: bool = False) -> Union[List[str], str]:
return parts


def get_runtime_env_info() -> Tuple[str, str]:
def get_runtime_env_info() -> Tuple[str, str, str]:
"""
Returns information about the current runtime environment.
This function collects and returns details about the machine architecture

This function collects and returns details about the machine architecture
and Python version being used by the application.

Returns:
Tuple[str, str]: A tuple containing:
Tuple[str, str, str]: A tuple containing:
- Machine type (e.g., 'arm64', 'ppc64le')
- System/OS name (e.g., ' Linux', 'Windows')
- Python version string
"""
machine = platform.machine()
system = platform.system()
python_version = platform.python_version()
return machine, python_version

return machine, system, python_version


def log_runtime_env_info() -> None:
"""
Logs debug information about the current runtime environment.

This function retrieves machine architecture and Python version information
using get_runtime_env_info() and logs it as a debug message.
"""
machine, python_version = get_runtime_env_info()
logger.debug(f"Runtime environment: Machine: {machine}, Python version: {python_version}")
machine, system, python_version = get_runtime_env_info()
logger.debug(
f"Runtime environment: Machine: {machine}, System: {system}, Python version: {python_version}"
)


def is_windows() -> bool:
"""
Checks if the current runtime environment is running on a Windows operating system.

Returns:
bool: True if the current runtime environment is Windows, False otherwise.
"""
system = get_runtime_env_info()[1].lower()
return system == "windows"


def is_ppc64() -> bool:
"""
Checks if the current runtime environment is running on ppc64 architecture.

Returns:
bool: True if the current runtime environment is on ppc64 architecture, False otherwise.
"""
machine = get_runtime_env_info()[0].lower()
return machine.startswith("ppc64")


def is_s390x() -> bool:
"""
Checks if the current runtime environment is running on s390x architecture.

Returns:
bool: True if the current runtime environment is on s390x architecture, False otherwise.
"""
machine = get_runtime_env_info()[0].lower()
return machine == "s390x"


# Made with Bob
Loading
Loading