diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md new file mode 100644 index 000000000..5f6ff0530 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +## Unreleased + +## Version 0.14b0 + +Released 2020-10-13 + +- Fix issue when specific metrics are not available in certain OS + ([#1207](https://github.com/open-telemetry/opentelemetry-python/pull/1207)) + +## Version 0.13b0 + +Released 2020-09-17 + +- Drop support for Python 3.4 + ([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099)) + +## Version 0.12b0 + +Released 2020-08-14 + +- Change package name to opentelemetry-instrumentation-system-metrics + ([#969](https://github.com/open-telemetry/opentelemetry-python/pull/969)) + +## 0.9b0 + +Released 2020-06-10 + +- Initial release (https://github.com/open-telemetry/opentelemetry-python/pull/652) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/LICENSE b/instrumentation/opentelemetry-instrumentation-system-metrics/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-system-metrics/MANIFEST.in new file mode 100644 index 000000000..aed3e3327 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/README.rst b/instrumentation/opentelemetry-instrumentation-system-metrics/README.rst new file mode 100644 index 000000000..fc984256b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry System Metrics Instrumentation +============================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-system-metrics.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-system-metrics/ + +Instrumentation to collect system performance metrics. + + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-system-metrics + + +References +---------- +* `OpenTelemetry System Metrics Instrumentation `_ +* `OpenTelemetry Project `_ + diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg new file mode 100644 index 000000000..750b5e07e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.cfg @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-instrumentation-system-metrics +description = OpenTelemetry System Metrics Instrumentation +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-system-metrics +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.15.dev0 + opentelemetry-sdk == 0.15.dev0 + psutil ~= 5.7.0 + +[options.extras_require] +test = + opentelemetry-test == 0.15.dev0 + +[options.packages.find] +where = src diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py new file mode 100644 index 000000000..f0bbf9eff --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py @@ -0,0 +1,31 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "system_metrics", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py new file mode 100644 index 000000000..71935aa8d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -0,0 +1,710 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Instrument to report system (CPU, memory, network) and +process (CPU, memory, garbage collection) metrics. By default, the +following metrics are configured: + +.. code:: python + + { + "system.cpu.time": ["idle", "user", "system", "irq"], + "system.cpu.utilization": ["idle", "user", "system", "irq"], + "system.memory.usage": ["used", "free", "cached"], + "system.memory.utilization": ["used", "free", "cached"], + "system.swap.usage": ["used", "free"], + "system.swap.utilization": ["used", "free"], + "system.disk.io": ["read", "write"], + "system.disk.operations": ["read", "write"], + "system.disk.time": ["read", "write"], + "system.disk.merged": ["read", "write"], + "system.network.dropped.packets": ["transmit", "receive"], + "system.network.packets": ["transmit", "receive"], + "system.network.errors": ["transmit", "receive"], + "system.network.io": ["trasmit", "receive"], + "system.network.connections": ["family", "type"], + "runtime.memory": ["rss", "vms"], + "runtime.cpu.time": ["user", "system"], + } + +Usage +----- + +.. code:: python + + from opentelemetry import metrics + from opentelemetry.instrumentation.system_metrics import SystemMetrics + from opentelemetry.sdk.metrics import MeterProvider, + from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter + + metrics.set_meter_provider(MeterProvider()) + exporter = ConsoleMetricsExporter() + SystemMetrics(exporter) + + # metrics are collected asynchronously + input("...") + + # to configure custom metrics + configuration = { + "system.memory.usage": ["used", "free", "cached"], + "system.cpu.time": ["idle", "user", "system", "irq"], + "system.network.io": ["trasmit", "receive"], + "runtime.memory": ["rss", "vms"], + "runtime.cpu.time": ["user", "system"], + } + SystemMetrics(exporter, config=configuration) + +API +--- +""" + +import gc +import os +import typing +from platform import python_implementation + +import psutil + +from opentelemetry import metrics +from opentelemetry.sdk.metrics import ( + SumObserver, + UpDownSumObserver, + ValueObserver, +) +from opentelemetry.sdk.metrics.export import MetricsExporter +from opentelemetry.sdk.metrics.export.controller import PushController +from opentelemetry.sdk.util import get_dict_as_key + + +class SystemMetrics: + # pylint: disable=too-many-statements + def __init__( + self, + exporter: MetricsExporter, + interval: int = 30, + labels: typing.Optional[typing.Dict[str, str]] = None, + config: typing.Optional[typing.Dict[str, typing.List[str]]] = None, + ): + self._labels = {} if labels is None else labels + self.meter = metrics.get_meter(__name__) + self.controller = PushController( + meter=self.meter, exporter=exporter, interval=interval + ) + self._python_implementation = python_implementation().lower() + if config is None: + self._config = { + "system.cpu.time": ["idle", "user", "system", "irq"], + "system.cpu.utilization": ["idle", "user", "system", "irq"], + "system.memory.usage": ["used", "free", "cached"], + "system.memory.utilization": ["used", "free", "cached"], + "system.swap.usage": ["used", "free"], + "system.swap.utilization": ["used", "free"], + # system.swap.page.faults: [], + # system.swap.page.operations: [], + "system.disk.io": ["read", "write"], + "system.disk.operations": ["read", "write"], + "system.disk.time": ["read", "write"], + "system.disk.merged": ["read", "write"], + # "system.filesystem.usage": [], + # "system.filesystem.utilization": [], + "system.network.dropped.packets": ["transmit", "receive"], + "system.network.packets": ["transmit", "receive"], + "system.network.errors": ["transmit", "receive"], + "system.network.io": ["trasmit", "receive"], + "system.network.connections": ["family", "type"], + "runtime.memory": ["rss", "vms"], + "runtime.cpu.time": ["user", "system"], + } + else: + self._config = config + + self._proc = psutil.Process(os.getpid()) + + self._system_cpu_time_labels = self._labels.copy() + self._system_cpu_utilization_labels = self._labels.copy() + + self._system_memory_usage_labels = self._labels.copy() + self._system_memory_utilization_labels = self._labels.copy() + + self._system_swap_usage_labels = self._labels.copy() + self._system_swap_utilization_labels = self._labels.copy() + # self._system_swap_page_faults = self._labels.copy() + # self._system_swap_page_operations = self._labels.copy() + + self._system_disk_io_labels = self._labels.copy() + self._system_disk_operations_labels = self._labels.copy() + self._system_disk_time_labels = self._labels.copy() + self._system_disk_merged_labels = self._labels.copy() + + # self._system_filesystem_usage_labels = self._labels.copy() + # self._system_filesystem_utilization_labels = self._labels.copy() + + self._system_network_dropped_packets_labels = self._labels.copy() + self._system_network_packets_labels = self._labels.copy() + self._system_network_errors_labels = self._labels.copy() + self._system_network_io_labels = self._labels.copy() + self._system_network_connections_labels = self._labels.copy() + + self._runtime_memory_labels = self._labels.copy() + self._runtime_cpu_time_labels = self._labels.copy() + self._runtime_gc_count_labels = self._labels.copy() + + self.meter.register_observer( + callback=self._get_system_cpu_time, + name="system.cpu.time", + description="System CPU time", + unit="seconds", + value_type=float, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_cpu_utilization, + name="system.cpu.utilization", + description="System CPU utilization", + unit="1", + value_type=float, + observer_type=ValueObserver, + ) + + self.meter.register_observer( + callback=self._get_system_memory_usage, + name="system.memory.usage", + description="System memory usage", + unit="bytes", + value_type=int, + observer_type=ValueObserver, + ) + + self.meter.register_observer( + callback=self._get_system_memory_utilization, + name="system.memory.utilization", + description="System memory utilization", + unit="1", + value_type=float, + observer_type=ValueObserver, + ) + + self.meter.register_observer( + callback=self._get_system_swap_usage, + name="system.swap.usage", + description="System swap usage", + unit="pages", + value_type=int, + observer_type=ValueObserver, + ) + + self.meter.register_observer( + callback=self._get_system_swap_utilization, + name="system.swap.utilization", + description="System swap utilization", + unit="1", + value_type=float, + observer_type=ValueObserver, + ) + + # self.meter.register_observer( + # callback=self._get_system_swap_page_faults, + # name="system.swap.page_faults", + # description="System swap page faults", + # unit="faults", + # value_type=int, + # observer_type=SumObserver, + # ) + + # self.meter.register_observer( + # callback=self._get_system_swap_page_operations, + # name="system.swap.page_operations", + # description="System swap page operations", + # unit="operations", + # value_type=int, + # observer_type=SumObserver, + # ) + + self.meter.register_observer( + callback=self._get_system_disk_io, + name="system.disk.io", + description="System disk IO", + unit="bytes", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_disk_operations, + name="system.disk.operations", + description="System disk operations", + unit="operations", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_disk_time, + name="system.disk.time", + description="System disk time", + unit="seconds", + value_type=float, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_disk_merged, + name="system.disk.merged", + description="System disk merged", + unit="1", + value_type=int, + observer_type=SumObserver, + ) + + # self.meter.register_observer( + # callback=self._get_system_filesystem_usage, + # name="system.filesystem.usage", + # description="System filesystem usage", + # unit="bytes", + # value_type=int, + # observer_type=ValueObserver, + # ) + + # self.meter.register_observer( + # callback=self._get_system_filesystem_utilization, + # name="system.filesystem.utilization", + # description="System filesystem utilization", + # unit="1", + # value_type=float, + # observer_type=ValueObserver, + # ) + + self.meter.register_observer( + callback=self._get_system_network_dropped_packets, + name="system.network.dropped_packets", + description="System network dropped_packets", + unit="packets", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_network_packets, + name="system.network.packets", + description="System network packets", + unit="packets", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_network_errors, + name="system.network.errors", + description="System network errors", + unit="errors", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_network_io, + name="system.network.io", + description="System network io", + unit="bytes", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_system_network_connections, + name="system.network.connections", + description="System network connections", + unit="connections", + value_type=int, + observer_type=UpDownSumObserver, + ) + + self.meter.register_observer( + callback=self._get_runtime_memory, + name="runtime.{}.memory".format(self._python_implementation), + description="Runtime {} memory".format( + self._python_implementation + ), + unit="bytes", + value_type=int, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_runtime_cpu_time, + name="runtime.{}.cpu_time".format(self._python_implementation), + description="Runtime {} CPU time".format( + self._python_implementation + ), + unit="seconds", + value_type=float, + observer_type=SumObserver, + ) + + self.meter.register_observer( + callback=self._get_runtime_gc_count, + name="runtime.{}.gc_count".format(self._python_implementation), + description="Runtime {} GC count".format( + self._python_implementation + ), + unit="bytes", + value_type=int, + observer_type=SumObserver, + ) + + def _get_system_cpu_time(self, observer: metrics.ValueObserver) -> None: + """Observer callback for system CPU time + + Args: + observer: the observer to update + """ + for cpu, times in enumerate(psutil.cpu_times(percpu=True)): + for metric in self._config["system.cpu.time"]: + if hasattr(times, metric): + self._system_cpu_time_labels["state"] = metric + self._system_cpu_time_labels["cpu"] = cpu + 1 + observer.observe( + getattr(times, metric), self._system_cpu_time_labels + ) + + def _get_system_cpu_utilization( + self, observer: metrics.ValueObserver + ) -> None: + """Observer callback for system CPU utilization + + Args: + observer: the observer to update + """ + + for cpu, times_percent in enumerate( + psutil.cpu_times_percent(percpu=True) + ): + for metric in self._config["system.cpu.utilization"]: + if hasattr(times_percent, metric): + self._system_cpu_utilization_labels["state"] = metric + self._system_cpu_utilization_labels["cpu"] = cpu + 1 + observer.observe( + getattr(times_percent, metric) / 100, + self._system_cpu_utilization_labels, + ) + + def _get_system_memory_usage( + self, observer: metrics.ValueObserver + ) -> None: + """Observer callback for memory usage + + Args: + observer: the observer to update + """ + virtual_memory = psutil.virtual_memory() + for metric in self._config["system.memory.usage"]: + self._system_memory_usage_labels["state"] = metric + if hasattr(virtual_memory, metric): + observer.observe( + getattr(virtual_memory, metric), + self._system_memory_usage_labels, + ) + + def _get_system_memory_utilization( + self, observer: metrics.ValueObserver + ) -> None: + """Observer callback for memory utilization + + Args: + observer: the observer to update + """ + system_memory = psutil.virtual_memory() + + for metric in self._config["system.memory.utilization"]: + self._system_memory_utilization_labels["state"] = metric + if hasattr(system_memory, metric): + observer.observe( + getattr(system_memory, metric) / system_memory.total, + self._system_memory_utilization_labels, + ) + + def _get_system_swap_usage(self, observer: metrics.ValueObserver) -> None: + """Observer callback for swap usage + + Args: + observer: the observer to update + """ + system_swap = psutil.swap_memory() + + for metric in self._config["system.swap.usage"]: + self._system_swap_usage_labels["state"] = metric + if hasattr(system_swap, metric): + observer.observe( + getattr(system_swap, metric), + self._system_swap_usage_labels, + ) + + def _get_system_swap_utilization( + self, observer: metrics.ValueObserver + ) -> None: + """Observer callback for swap utilization + + Args: + observer: the observer to update + """ + system_swap = psutil.swap_memory() + + for metric in self._config["system.swap.utilization"]: + if hasattr(system_swap, metric): + self._system_swap_utilization_labels["state"] = metric + observer.observe( + getattr(system_swap, metric) / system_swap.total, + self._system_swap_utilization_labels, + ) + + # TODO Add _get_system_swap_page_faults + # TODO Add _get_system_swap_page_operations + + def _get_system_disk_io(self, observer: metrics.SumObserver) -> None: + """Observer callback for disk IO + + Args: + observer: the observer to update + """ + for device, counters in psutil.disk_io_counters(perdisk=True).items(): + for metric in self._config["system.disk.io"]: + if hasattr(counters, "{}_bytes".format(metric)): + self._system_disk_io_labels["device"] = device + self._system_disk_io_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_bytes".format(metric)), + self._system_disk_io_labels, + ) + + def _get_system_disk_operations( + self, observer: metrics.SumObserver + ) -> None: + """Observer callback for disk operations + + Args: + observer: the observer to update + """ + for device, counters in psutil.disk_io_counters(perdisk=True).items(): + for metric in self._config["system.disk.operations"]: + if hasattr(counters, "{}_count".format(metric)): + self._system_disk_operations_labels["device"] = device + self._system_disk_operations_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_count".format(metric)), + self._system_disk_operations_labels, + ) + + def _get_system_disk_time(self, observer: metrics.SumObserver) -> None: + """Observer callback for disk time + + Args: + observer: the observer to update + """ + for device, counters in psutil.disk_io_counters(perdisk=True).items(): + for metric in self._config["system.disk.time"]: + if hasattr(counters, "{}_time".format(metric)): + self._system_disk_time_labels["device"] = device + self._system_disk_time_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_time".format(metric)) / 1000, + self._system_disk_time_labels, + ) + + def _get_system_disk_merged(self, observer: metrics.SumObserver) -> None: + """Observer callback for disk merged operations + + Args: + observer: the observer to update + """ + + # FIXME The units in the spec is 1, it seems like it should be + # operations or the value type should be Double + + for device, counters in psutil.disk_io_counters(perdisk=True).items(): + for metric in self._config["system.disk.time"]: + if hasattr(counters, "{}_merged_count".format(metric)): + self._system_disk_merged_labels["device"] = device + self._system_disk_merged_labels["direction"] = metric + observer.observe( + getattr(counters, "{}_merged_count".format(metric)), + self._system_disk_merged_labels, + ) + + # TODO Add _get_system_filesystem_usage + # TODO Add _get_system_filesystem_utilization + # TODO Filesystem information can be obtained with os.statvfs in Unix-like + # OSs, how to do the same in Windows? + + def _get_system_network_dropped_packets( + self, observer: metrics.SumObserver + ) -> None: + """Observer callback for network dropped packets + + Args: + observer: the observer to update + """ + + for device, counters in psutil.net_io_counters(pernic=True).items(): + for metric in self._config["system.network.dropped.packets"]: + in_out = {"receive": "in", "transmit": "out"}[metric] + if hasattr(counters, "drop{}".format(in_out)): + self._system_network_dropped_packets_labels[ + "device" + ] = device + self._system_network_dropped_packets_labels[ + "direction" + ] = metric + observer.observe( + getattr(counters, "drop{}".format(in_out)), + self._system_network_dropped_packets_labels, + ) + + def _get_system_network_packets( + self, observer: metrics.SumObserver + ) -> None: + """Observer callback for network packets + + Args: + observer: the observer to update + """ + + for device, counters in psutil.net_io_counters(pernic=True).items(): + for metric in self._config["system.network.dropped.packets"]: + recv_sent = {"receive": "recv", "transmit": "sent"}[metric] + if hasattr(counters, "packets_{}".format(recv_sent)): + self._system_network_packets_labels["device"] = device + self._system_network_packets_labels["direction"] = metric + observer.observe( + getattr(counters, "packets_{}".format(recv_sent)), + self._system_network_packets_labels, + ) + + def _get_system_network_errors( + self, observer: metrics.SumObserver + ) -> None: + """Observer callback for network errors + + Args: + observer: the observer to update + """ + for device, counters in psutil.net_io_counters(pernic=True).items(): + for metric in self._config["system.network.errors"]: + in_out = {"receive": "in", "transmit": "out"}[metric] + if hasattr(counters, "err{}".format(in_out)): + self._system_network_errors_labels["device"] = device + self._system_network_errors_labels["direction"] = metric + observer.observe( + getattr(counters, "err{}".format(in_out)), + self._system_network_errors_labels, + ) + + def _get_system_network_io(self, observer: metrics.SumObserver) -> None: + """Observer callback for network IO + + Args: + observer: the observer to update + """ + + for device, counters in psutil.net_io_counters(pernic=True).items(): + for metric in self._config["system.network.dropped.packets"]: + recv_sent = {"receive": "recv", "transmit": "sent"}[metric] + if hasattr(counters, "bytes_{}".format(recv_sent)): + self._system_network_io_labels["device"] = device + self._system_network_io_labels["direction"] = metric + observer.observe( + getattr(counters, "bytes_{}".format(recv_sent)), + self._system_network_io_labels, + ) + + def _get_system_network_connections( + self, observer: metrics.UpDownSumObserver + ) -> None: + """Observer callback for network connections + + Args: + observer: the observer to update + """ + # TODO How to find the device identifier for a particular + # connection? + + connection_counters = {} + + for net_connection in psutil.net_connections(): + for metric in self._config["system.network.connections"]: + self._system_network_connections_labels["protocol"] = { + 1: "tcp", + 2: "udp", + }[net_connection.type.value] + self._system_network_connections_labels[ + "state" + ] = net_connection.status + self._system_network_connections_labels[metric] = getattr( + net_connection, metric + ) + + connection_counters_key = get_dict_as_key( + self._system_network_connections_labels + ) + + if connection_counters_key in connection_counters.keys(): + connection_counters[connection_counters_key]["counter"] += 1 + else: + connection_counters[connection_counters_key] = { + "counter": 1, + "labels": self._system_network_connections_labels.copy(), + } + + for connection_counter in connection_counters.values(): + observer.observe( + connection_counter["counter"], connection_counter["labels"], + ) + + def _get_runtime_memory(self, observer: metrics.SumObserver) -> None: + """Observer callback for runtime memory + + Args: + observer: the observer to update + """ + proc_memory = self._proc.memory_info() + for metric in self._config["runtime.memory"]: + if hasattr(proc_memory, metric): + self._runtime_memory_labels["type"] = metric + observer.observe( + getattr(proc_memory, metric), self._runtime_memory_labels, + ) + + def _get_runtime_cpu_time(self, observer: metrics.SumObserver) -> None: + """Observer callback for runtime CPU time + + Args: + observer: the observer to update + """ + proc_cpu = self._proc.cpu_times() + for metric in self._config["runtime.cpu.time"]: + if hasattr(proc_cpu, metric): + self._runtime_cpu_time_labels["type"] = metric + observer.observe( + getattr(proc_cpu, metric), self._runtime_cpu_time_labels, + ) + + def _get_runtime_gc_count(self, observer: metrics.SumObserver) -> None: + """Observer callback for garbage collection + + Args: + observer: the observer to update + """ + for index, count in enumerate(gc.get_count()): + self._runtime_gc_count_labels["count"] = str(index) + observer.observe(count, self._runtime_gc_count_labels) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py new file mode 100644 index 000000000..e7b342d64 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.15.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py new file mode 100644 index 000000000..2f155383f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -0,0 +1,703 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +from collections import namedtuple +from platform import python_implementation +from unittest import mock + +from opentelemetry import metrics +from opentelemetry.instrumentation.system_metrics import SystemMetrics +from opentelemetry.sdk.metrics.export.aggregate import ValueObserverAggregator +from opentelemetry.test.test_base import TestBase + + +class TestSystemMetrics(TestBase): + def setUp(self): + super().setUp() + self.memory_metrics_exporter.clear() + self.implementation = python_implementation().lower() + + def test_system_metrics_constructor(self): + # ensure the observers have been registered + meter = metrics.get_meter(__name__) + with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: + mock_get_meter.return_value = meter + SystemMetrics(self.memory_metrics_exporter) + + self.assertEqual(len(meter.observers), 18) + + observer_names = [ + "system.cpu.time", + "system.cpu.utilization", + "system.memory.usage", + "system.memory.utilization", + "system.swap.usage", + "system.swap.utilization", + "system.disk.io", + "system.disk.operations", + "system.disk.time", + "system.disk.merged", + "system.network.dropped_packets", + "system.network.packets", + "system.network.errors", + "system.network.io", + "system.network.connections", + "runtime.{}.memory".format(self.implementation), + "runtime.{}.cpu_time".format(self.implementation), + "runtime.{}.gc_count".format(self.implementation), + ] + + for observer in meter.observers: + self.assertIn(observer.name, observer_names) + observer_names.remove(observer.name) + + def _assert_metrics(self, observer_name, system_metrics, expected): + system_metrics.controller.tick() + assertions = 0 + for ( + metric + ) in ( + self.memory_metrics_exporter._exported_metrics # pylint: disable=protected-access + ): + if ( + metric.labels in expected + and metric.instrument.name == observer_name + ): + self.assertEqual( + metric.aggregator.checkpoint, expected[metric.labels], + ) + assertions += 1 + self.assertEqual(len(expected), assertions) + + def _test_metrics(self, observer_name, expected): + meter = self.meter_provider.get_meter(__name__) + + with mock.patch("opentelemetry.metrics.get_meter") as mock_get_meter: + mock_get_meter.return_value = meter + system_metrics = SystemMetrics(self.memory_metrics_exporter) + self._assert_metrics(observer_name, system_metrics, expected) + + # When this test case is executed, _get_system_cpu_utilization gets run + # too because of the controller thread which runs all observers. This patch + # is added here to stop a warning that would otherwise be raised. + # pylint: disable=unused-argument + @mock.patch("psutil.cpu_times_percent") + @mock.patch("psutil.cpu_times") + def test_system_cpu_time(self, mock_cpu_times, mock_cpu_times_percent): + CPUTimes = namedtuple("CPUTimes", ["idle", "user", "system", "irq"]) + mock_cpu_times.return_value = [ + CPUTimes(idle=1.2, user=3.4, system=5.6, irq=7.8), + CPUTimes(idle=1.2, user=3.4, system=5.6, irq=7.8), + ] + + expected = { + (("cpu", 1), ("state", "idle"),): 1.2, + (("cpu", 1), ("state", "user"),): 3.4, + (("cpu", 1), ("state", "system"),): 5.6, + (("cpu", 1), ("state", "irq"),): 7.8, + (("cpu", 2), ("state", "idle"),): 1.2, + (("cpu", 2), ("state", "user"),): 3.4, + (("cpu", 2), ("state", "system"),): 5.6, + (("cpu", 2), ("state", "irq"),): 7.8, + } + self._test_metrics("system.cpu.time", expected) + + @mock.patch("psutil.cpu_times_percent") + def test_system_cpu_utilization(self, mock_cpu_times_percent): + CPUTimesPercent = namedtuple( + "CPUTimesPercent", ["idle", "user", "system", "irq"] + ) + mock_cpu_times_percent.return_value = [ + CPUTimesPercent(idle=1.2, user=3.4, system=5.6, irq=7.8), + CPUTimesPercent(idle=1.2, user=3.4, system=5.6, irq=7.8), + ] + + expected = { + (("cpu", 1), ("state", "idle"),): ValueObserverAggregator._TYPE( + min=1.2 / 100, + max=1.2 / 100, + sum=1.2 / 100, + count=1, + last=1.2 / 100, + ), + (("cpu", 1), ("state", "user"),): ValueObserverAggregator._TYPE( + min=3.4 / 100, + max=3.4 / 100, + sum=3.4 / 100, + count=1, + last=3.4 / 100, + ), + (("cpu", 1), ("state", "system"),): ValueObserverAggregator._TYPE( + min=5.6 / 100, + max=5.6 / 100, + sum=5.6 / 100, + count=1, + last=5.6 / 100, + ), + (("cpu", 1), ("state", "irq"),): ValueObserverAggregator._TYPE( + min=7.8 / 100, + max=7.8 / 100, + sum=7.8 / 100, + count=1, + last=7.8 / 100, + ), + (("cpu", 2), ("state", "idle"),): ValueObserverAggregator._TYPE( + min=1.2 / 100, + max=1.2 / 100, + sum=1.2 / 100, + count=1, + last=1.2 / 100, + ), + (("cpu", 2), ("state", "user"),): ValueObserverAggregator._TYPE( + min=3.4 / 100, + max=3.4 / 100, + sum=3.4 / 100, + count=1, + last=3.4 / 100, + ), + (("cpu", 2), ("state", "system"),): ValueObserverAggregator._TYPE( + min=5.6 / 100, + max=5.6 / 100, + sum=5.6 / 100, + count=1, + last=5.6 / 100, + ), + (("cpu", 2), ("state", "irq"),): ValueObserverAggregator._TYPE( + min=7.8 / 100, + max=7.8 / 100, + sum=7.8 / 100, + count=1, + last=7.8 / 100, + ), + } + self._test_metrics("system.cpu.utilization", expected) + + @mock.patch("psutil.virtual_memory") + def test_system_memory_usage(self, mock_virtual_memory): + VirtualMemory = namedtuple( + "VirtualMemory", ["used", "free", "cached", "total"] + ) + mock_virtual_memory.return_value = VirtualMemory( + used=1, free=2, cached=3, total=4 + ) + + expected = { + (("state", "used"),): ValueObserverAggregator._TYPE( + min=1, max=1, sum=1, count=1, last=1 + ), + (("state", "free"),): ValueObserverAggregator._TYPE( + min=2, max=2, sum=2, count=1, last=2 + ), + (("state", "cached"),): ValueObserverAggregator._TYPE( + min=3, max=3, sum=3, count=1, last=3 + ), + } + self._test_metrics("system.memory.usage", expected) + + @mock.patch("psutil.virtual_memory") + def test_system_memory_utilization(self, mock_virtual_memory): + VirtualMemory = namedtuple( + "VirtualMemory", ["used", "free", "cached", "total"] + ) + mock_virtual_memory.return_value = VirtualMemory( + used=1, free=2, cached=3, total=4 + ) + + expected = { + (("state", "used"),): ValueObserverAggregator._TYPE( + min=1 / 4, max=1 / 4, sum=1 / 4, count=1, last=1 / 4 + ), + (("state", "free"),): ValueObserverAggregator._TYPE( + min=2 / 4, max=2 / 4, sum=2 / 4, count=1, last=2 / 4 + ), + (("state", "cached"),): ValueObserverAggregator._TYPE( + min=3 / 4, max=3 / 4, sum=3 / 4, count=1, last=3 / 4 + ), + } + self._test_metrics("system.memory.utilization", expected) + + @mock.patch("psutil.swap_memory") + def test_system_swap_usage(self, mock_swap_memory): + SwapMemory = namedtuple("SwapMemory", ["used", "free", "total"]) + mock_swap_memory.return_value = SwapMemory(used=1, free=2, total=3) + + expected = { + (("state", "used"),): ValueObserverAggregator._TYPE( + min=1, max=1, sum=1, count=1, last=1 + ), + (("state", "free"),): ValueObserverAggregator._TYPE( + min=2, max=2, sum=2, count=1, last=2 + ), + } + self._test_metrics("system.swap.usage", expected) + + @mock.patch("psutil.swap_memory") + def test_system_swap_utilization(self, mock_swap_memory): + SwapMemory = namedtuple("SwapMemory", ["used", "free", "total"]) + mock_swap_memory.return_value = SwapMemory(used=1, free=2, total=3) + + expected = { + (("state", "used"),): ValueObserverAggregator._TYPE( + min=1 / 3, max=1 / 3, sum=1 / 3, count=1, last=1 / 3 + ), + (("state", "free"),): ValueObserverAggregator._TYPE( + min=2 / 3, max=2 / 3, sum=2 / 3, count=1, last=2 / 3 + ), + } + self._test_metrics("system.swap.utilization", expected) + + @mock.patch("psutil.disk_io_counters") + def test_system_disk_io(self, mock_disk_io_counters): + DiskIO = namedtuple( + "DiskIO", + [ + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + "read_merged_count", + "write_merged_count", + ], + ) + mock_disk_io_counters.return_value = { + "sda": DiskIO( + read_count=1, + write_count=2, + read_bytes=3, + write_bytes=4, + read_time=5, + write_time=6, + read_merged_count=7, + write_merged_count=8, + ), + "sdb": DiskIO( + read_count=9, + write_count=10, + read_bytes=11, + write_bytes=12, + read_time=13, + write_time=14, + read_merged_count=15, + write_merged_count=16, + ), + } + + expected = { + (("device", "sda"), ("direction", "read"),): 3, + (("device", "sda"), ("direction", "write"),): 4, + (("device", "sdb"), ("direction", "read"),): 11, + (("device", "sdb"), ("direction", "write"),): 12, + } + self._test_metrics("system.disk.io", expected) + + @mock.patch("psutil.disk_io_counters") + def test_system_disk_operations(self, mock_disk_io_counters): + DiskIO = namedtuple( + "DiskIO", + [ + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + "read_merged_count", + "write_merged_count", + ], + ) + mock_disk_io_counters.return_value = { + "sda": DiskIO( + read_count=1, + write_count=2, + read_bytes=3, + write_bytes=4, + read_time=5, + write_time=6, + read_merged_count=7, + write_merged_count=8, + ), + "sdb": DiskIO( + read_count=9, + write_count=10, + read_bytes=11, + write_bytes=12, + read_time=13, + write_time=14, + read_merged_count=15, + write_merged_count=16, + ), + } + + expected = { + (("device", "sda"), ("direction", "read"),): 1, + (("device", "sda"), ("direction", "write"),): 2, + (("device", "sdb"), ("direction", "read"),): 9, + (("device", "sdb"), ("direction", "write"),): 10, + } + self._test_metrics("system.disk.operations", expected) + + @mock.patch("psutil.disk_io_counters") + def test_system_disk_time(self, mock_disk_io_counters): + DiskIO = namedtuple( + "DiskIO", + [ + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + "read_merged_count", + "write_merged_count", + ], + ) + mock_disk_io_counters.return_value = { + "sda": DiskIO( + read_count=1, + write_count=2, + read_bytes=3, + write_bytes=4, + read_time=5, + write_time=6, + read_merged_count=7, + write_merged_count=8, + ), + "sdb": DiskIO( + read_count=9, + write_count=10, + read_bytes=11, + write_bytes=12, + read_time=13, + write_time=14, + read_merged_count=15, + write_merged_count=16, + ), + } + + expected = { + (("device", "sda"), ("direction", "read"),): 5 / 1000, + (("device", "sda"), ("direction", "write"),): 6 / 1000, + (("device", "sdb"), ("direction", "read"),): 13 / 1000, + (("device", "sdb"), ("direction", "write"),): 14 / 1000, + } + self._test_metrics("system.disk.time", expected) + + @mock.patch("psutil.disk_io_counters") + def test_system_disk_merged(self, mock_disk_io_counters): + DiskIO = namedtuple( + "DiskIO", + [ + "read_count", + "write_count", + "read_bytes", + "write_bytes", + "read_time", + "write_time", + "read_merged_count", + "write_merged_count", + ], + ) + mock_disk_io_counters.return_value = { + "sda": DiskIO( + read_count=1, + write_count=2, + read_bytes=3, + write_bytes=4, + read_time=5, + write_time=6, + read_merged_count=7, + write_merged_count=8, + ), + "sdb": DiskIO( + read_count=9, + write_count=10, + read_bytes=11, + write_bytes=12, + read_time=13, + write_time=14, + read_merged_count=15, + write_merged_count=16, + ), + } + + expected = { + (("device", "sda"), ("direction", "read"),): 7, + (("device", "sda"), ("direction", "write"),): 8, + (("device", "sdb"), ("direction", "read"),): 15, + (("device", "sdb"), ("direction", "write"),): 16, + } + self._test_metrics("system.disk.merged", expected) + + @mock.patch("psutil.net_io_counters") + def test_system_network_dropped_packets(self, mock_net_io_counters): + NetIO = namedtuple( + "NetIO", + [ + "dropin", + "dropout", + "packets_sent", + "packets_recv", + "errin", + "errout", + "bytes_sent", + "bytes_recv", + ], + ) + mock_net_io_counters.return_value = { + "eth0": NetIO( + dropin=1, + dropout=2, + packets_sent=3, + packets_recv=4, + errin=5, + errout=6, + bytes_sent=7, + bytes_recv=8, + ), + "eth1": NetIO( + dropin=9, + dropout=10, + packets_sent=11, + packets_recv=12, + errin=13, + errout=14, + bytes_sent=15, + bytes_recv=16, + ), + } + + expected = { + (("device", "eth0"), ("direction", "receive"),): 1, + (("device", "eth0"), ("direction", "transmit"),): 2, + (("device", "eth1"), ("direction", "receive"),): 9, + (("device", "eth1"), ("direction", "transmit"),): 10, + } + self._test_metrics("system.network.dropped_packets", expected) + + @mock.patch("psutil.net_io_counters") + def test_system_network_packets(self, mock_net_io_counters): + NetIO = namedtuple( + "NetIO", + [ + "dropin", + "dropout", + "packets_sent", + "packets_recv", + "errin", + "errout", + "bytes_sent", + "bytes_recv", + ], + ) + mock_net_io_counters.return_value = { + "eth0": NetIO( + dropin=1, + dropout=2, + packets_sent=3, + packets_recv=4, + errin=5, + errout=6, + bytes_sent=7, + bytes_recv=8, + ), + "eth1": NetIO( + dropin=9, + dropout=10, + packets_sent=11, + packets_recv=12, + errin=13, + errout=14, + bytes_sent=15, + bytes_recv=16, + ), + } + + expected = { + (("device", "eth0"), ("direction", "receive"),): 4, + (("device", "eth0"), ("direction", "transmit"),): 3, + (("device", "eth1"), ("direction", "receive"),): 12, + (("device", "eth1"), ("direction", "transmit"),): 11, + } + self._test_metrics("system.network.packets", expected) + + @mock.patch("psutil.net_io_counters") + def test_system_network_errors(self, mock_net_io_counters): + NetIO = namedtuple( + "NetIO", + [ + "dropin", + "dropout", + "packets_sent", + "packets_recv", + "errin", + "errout", + "bytes_sent", + "bytes_recv", + ], + ) + mock_net_io_counters.return_value = { + "eth0": NetIO( + dropin=1, + dropout=2, + packets_sent=3, + packets_recv=4, + errin=5, + errout=6, + bytes_sent=7, + bytes_recv=8, + ), + "eth1": NetIO( + dropin=9, + dropout=10, + packets_sent=11, + packets_recv=12, + errin=13, + errout=14, + bytes_sent=15, + bytes_recv=16, + ), + } + + expected = { + (("device", "eth0"), ("direction", "receive"),): 5, + (("device", "eth0"), ("direction", "transmit"),): 6, + (("device", "eth1"), ("direction", "receive"),): 13, + (("device", "eth1"), ("direction", "transmit"),): 14, + } + self._test_metrics("system.network.errors", expected) + + @mock.patch("psutil.net_io_counters") + def test_system_network_io(self, mock_net_io_counters): + NetIO = namedtuple( + "NetIO", + [ + "dropin", + "dropout", + "packets_sent", + "packets_recv", + "errin", + "errout", + "bytes_sent", + "bytes_recv", + ], + ) + mock_net_io_counters.return_value = { + "eth0": NetIO( + dropin=1, + dropout=2, + packets_sent=3, + packets_recv=4, + errin=5, + errout=6, + bytes_sent=7, + bytes_recv=8, + ), + "eth1": NetIO( + dropin=9, + dropout=10, + packets_sent=11, + packets_recv=12, + errin=13, + errout=14, + bytes_sent=15, + bytes_recv=16, + ), + } + + expected = { + (("device", "eth0"), ("direction", "receive"),): 8, + (("device", "eth0"), ("direction", "transmit"),): 7, + (("device", "eth1"), ("direction", "receive"),): 16, + (("device", "eth1"), ("direction", "transmit"),): 15, + } + self._test_metrics("system.network.io", expected) + + @mock.patch("psutil.net_connections") + def test_system_network_connections(self, mock_net_connections): + NetConnection = namedtuple( + "NetworkConnection", ["family", "type", "status"] + ) + Type = namedtuple("Type", ["value"]) + mock_net_connections.return_value = [ + NetConnection(family=1, status="ESTABLISHED", type=Type(value=2),), + NetConnection(family=1, status="ESTABLISHED", type=Type(value=1),), + ] + + expected = { + ( + ("family", 1), + ("protocol", "udp"), + ("state", "ESTABLISHED"), + ("type", Type(value=2)), + ): 1, + ( + ("family", 1), + ("protocol", "tcp"), + ("state", "ESTABLISHED"), + ("type", Type(value=1)), + ): 1, + } + self._test_metrics("system.network.connections", expected) + + @mock.patch("psutil.Process.memory_info") + def test_runtime_memory(self, mock_process_memory_info): + + PMem = namedtuple("PMem", ["rss", "vms"]) + + mock_process_memory_info.configure_mock( + **{"return_value": PMem(rss=1, vms=2)} + ) + + expected = { + (("type", "rss"),): 1, + (("type", "vms"),): 2, + } + self._test_metrics( + "runtime.{}.memory".format(self.implementation), expected + ) + + @mock.patch("psutil.Process.cpu_times") + def test_runtime_cpu_time(self, mock_process_cpu_times): + + PCPUTimes = namedtuple("PCPUTimes", ["user", "system"]) + + mock_process_cpu_times.configure_mock( + **{"return_value": PCPUTimes(user=1.1, system=2.2)} + ) + + expected = { + (("type", "user"),): 1.1, + (("type", "system"),): 2.2, + } + self._test_metrics( + "runtime.{}.cpu_time".format(self.implementation), expected + ) + + @mock.patch("gc.get_count") + def test_runtime_get_count(self, mock_gc_get_count): + + mock_gc_get_count.configure_mock(**{"return_value": (1, 2, 3)}) + + expected = { + (("count", "0"),): 1, + (("count", "1"),): 2, + (("count", "2"),): 3, + } + self._test_metrics( + "runtime.{}.gc_count".format(self.implementation), expected + )