detector.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. # Copyright 2024 The Chromium Authors
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Defines the ResourceDetector to capture resource properties."""
  5. import logging
  6. import os
  7. from pathlib import Path
  8. import platform
  9. import sys
  10. from typing import Optional, Sequence
  11. from opentelemetry.sdk import resources
  12. CPU_ARCHITECTURE = "cpu.architecture"
  13. CPU_NAME = "cpu.name"
  14. CPU_COUNT = "cpu.count"
  15. HOST_TYPE = "host.type"
  16. MEMORY_SWAP_TOTAL = "memory.swap.total"
  17. MEMORY_TOTAL = "memory.total"
  18. PROCESS_CWD = "process.cwd"
  19. PROCESS_RUNTIME_API_VERSION = "process.runtime.apiversion"
  20. PROCESS_ENV = "process.env"
  21. OS_NAME = "os.name"
  22. DMI_PATH = Path("/sys/class/dmi/id/product_name")
  23. GCE_DMI = "Google Compute Engine"
  24. PROC_MEMINFO_PATH = Path("/proc/meminfo")
  25. class ProcessDetector(resources.ResourceDetector):
  26. """ResourceDetector to capture information about the process."""
  27. def __init__(self, allowed_env: Optional[Sequence[str]] = None) -> None:
  28. super().__init__()
  29. self._allowed_env = allowed_env or ["USE"]
  30. def detect(self) -> resources.Resource:
  31. env = os.environ
  32. resource = {
  33. PROCESS_CWD: os.getcwd(),
  34. PROCESS_RUNTIME_API_VERSION: sys.api_version,
  35. resources.PROCESS_PID: os.getpid(),
  36. resources.PROCESS_OWNER: os.geteuid(),
  37. resources.PROCESS_EXECUTABLE_NAME: Path(sys.executable).name,
  38. resources.PROCESS_EXECUTABLE_PATH: sys.executable,
  39. resources.PROCESS_COMMAND: sys.argv[0],
  40. resources.PROCESS_COMMAND_ARGS: sys.argv[1:],
  41. }
  42. resource.update({
  43. f"{PROCESS_ENV}.{k}": env[k]
  44. for k in self._allowed_env if k in env
  45. })
  46. return resources.Resource(resource)
  47. class SystemDetector(resources.ResourceDetector):
  48. """ResourceDetector to capture information about system."""
  49. def detect(self) -> resources.Resource:
  50. host_type = "UNKNOWN"
  51. if DMI_PATH.exists():
  52. host_type = DMI_PATH.read_text(encoding="utf-8")
  53. mem_info = MemoryInfo()
  54. resource = {
  55. CPU_ARCHITECTURE: platform.machine(),
  56. CPU_COUNT: os.cpu_count(),
  57. CPU_NAME: platform.processor(),
  58. HOST_TYPE: host_type.strip(),
  59. MEMORY_SWAP_TOTAL: mem_info.total_swap_memory,
  60. MEMORY_TOTAL: mem_info.total_physical_ram,
  61. OS_NAME: os.name,
  62. resources.OS_TYPE: platform.system(),
  63. resources.OS_DESCRIPTION: platform.platform(),
  64. }
  65. return resources.Resource(resource)
  66. class MemoryInfo:
  67. """Read machine memory info from /proc/meminfo."""
  68. # Prefixes for the /proc/meminfo file that we care about.
  69. MEMINFO_VIRTUAL_MEMORY_TOTAL = "VmallocTotal"
  70. MEMINFO_PHYSICAL_RAM_TOTAL = "MemTotal"
  71. MEMINFO_SWAP_MEMORY_TOTAL = "SwapTotal"
  72. def __init__(self) -> None:
  73. self._total_physical_ram = 0
  74. self._total_virtual_memory = 0
  75. self._total_swap_memory = 0
  76. try:
  77. contents = PROC_MEMINFO_PATH.read_text(encoding="utf-8")
  78. except OSError as e:
  79. logging.warning("Encountered an issue reading /proc/meminfo: %s", e)
  80. return
  81. for line in contents.splitlines():
  82. if line.startswith(self.MEMINFO_SWAP_MEMORY_TOTAL):
  83. self._total_swap_memory = self._get_mem_value(line)
  84. elif line.startswith(self.MEMINFO_VIRTUAL_MEMORY_TOTAL):
  85. self._total_virtual_memory = self._get_mem_value(line)
  86. elif line.startswith(self.MEMINFO_PHYSICAL_RAM_TOTAL):
  87. self._total_physical_ram = self._get_mem_value(line)
  88. @property
  89. def total_physical_ram(self) -> int:
  90. return self._total_physical_ram
  91. @property
  92. def total_virtual_memory(self) -> int:
  93. return self._total_virtual_memory
  94. @property
  95. def total_swap_memory(self) -> int:
  96. return self._total_swap_memory
  97. def _get_mem_value(self, line: str) -> int:
  98. """Reads an individual line from /proc/meminfo and returns the size.
  99. This function also converts the read value from kibibytes to bytes
  100. when the read value has a unit provided for memory size.
  101. The specification information for /proc files, including meminfo, can
  102. be found at
  103. https://www.kernel.org/doc/Documentation/filesystems/proc.txt.
  104. Args:
  105. line: The text line read from /proc/meminfo.
  106. Returns:
  107. The integer value after conversion.
  108. """
  109. components = line.split()
  110. if len(components) == 1:
  111. logging.warning(
  112. "Unexpected /proc/meminfo entry with no label:number value was "
  113. "provided. Value read: '%s'",
  114. line,
  115. )
  116. return 0
  117. size = int(components[1])
  118. if len(components) == 2:
  119. return size
  120. # The RHEL and kernel.org specs for /proc/meminfo doesn't give any
  121. # indication that a memory unit besides kB (kibibytes) is expected,
  122. # except in the cases of page counts, where no unit is provided.
  123. if components[2] != "kB":
  124. logging.warning(
  125. "Unit for memory consumption in /proc/meminfo does "
  126. "not conform to expectations. Please review the "
  127. "read value: %s",
  128. line,
  129. )
  130. return size * 1024