Repository: open-telemetry/opentelemetry-python-contrib

Issue:
[WIP] Add support for AsyncElasticsearch
# Description

The current elasticsearch instrumentation does not support version 8.x (see https://github.com/open-telemetry/opentelemetry-python-contrib/issues/620), which this PR does not fix, but it also does not support the async capabilities introduced in elasticsearch-python version 7.8.0 (https://elasticsearch-py.readthedocs.io/en/7.x/async.html#using-asyncio-with-elasticsearch). This PR aims to fix the latter, until a solution for the former is implemented.

This adds a wrapping of the `AsyncTransport` in addition to the already existing wrapping of `Transport`.

Progress:
- [x] Wrap AsyncTransport
- [x] Extract common parts
- [ ] Ensure it only runs if `AsyncTransport` is actually available (elasticsearch>=7.8.0)
- [ ] Add tests

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update (probably?)

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

- [ ] Original tests pass
- [x] Manually checked that traces are delivered with elasticsearch==7.17.1 both with sync and async clients
- [ ] Automatic tests for older versions of elasticsearch
- [ ] Automatic tests for newer versions of elasticsearch

# Does This PR Require a Core Repo Change?

- [ ] Yes. - Link to PR: 
- [x] No.

# Checklist:

See [contributing.md](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/CONTRIBUTING.md) for styleguide, changelog guidelines, and more.

- [x] Followed the style guidelines of this project
- [ ] Changelogs have been updated
- [ ] Unit tests have been added
- [ ] Documentation has been updated



Discussion:

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py b/instrumentation/opentelemetry-instrumentation-system-metrics/setup.py
new file mode 100644
--- /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
--- /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
--- /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"


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/__init__.py
new file mode 100644
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
--- /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
+        )