Repository: prometheus/client_python
Pull request #152 (2017-03-28 12:55:36)

Add PlatformCollector to collect information about the platform
In the same vein as https://github.com/prometheus/client_java/commit/bdc6f2507a81029c1aec6cd8b5276661bb6c7ee1 and https://github.com/siimon/prom-client/commit/ee561e28e14a62b0ef069f525174ee9a3b66d4d7, this PR adds Python version and some other metadata as labels on a small set of metrics.

This has turned out to be very useful for creating common dashboards across implementation languages, allowing the dashboard to extract metadata from the metrics exposed.

Python, machine and system information is collected once at startup, and
exposed as labels on a metric with value 1.

brian-brazil: Machine level information should come from the node exporter, it's not the concern of a python application. Cut this down to just the python stuff.
mortenlj: Yeah, I can see the point about the machine info, I'll remove that.

I'll remove the windows, linux, darwin stuff too, as that is also something the node exporter can handle.

What do you think about the java info? If you are using jython, which java version your application is using becomes relevant, and it's not something the node exporter can tell you (it might tell you which java versions are installed, but not necessarily which one is used to run your application).
brian-brazil: Keeping Jython stats makes sense. There's an open question on how to handle a mix of Jython and client_java stats, but noone has asked about it yet.

Patch:
diff --git a/README.md b/README.md
index 430dffa..443dc7c 100644
--- a/README.md
+++ b/README.md
@@ -206,6 +206,13 @@ other processes, for example:
 ProcessCollector(namespace='mydaemon', pid=lambda: open('/var/run/daemon.pid').read())
 ```
 
+### Platform Collector
+
+The client also automatically exports some metadata about Python. If using Jython,
+metadata about the JVM in use is also included. This information is available as 
+labels on the `python_info` metric. The value of the metric is 1, since it is the 
+labels that carry information.
+
 ## Exporting
 
 There are several options for exporting metrics.
diff --git a/prometheus_client/__init__.py b/prometheus_client/__init__.py
index 358146a..b270a86 100644
--- a/prometheus_client/__init__.py
+++ b/prometheus_client/__init__.py
@@ -3,6 +3,7 @@
 from . import core
 from . import exposition
 from . import process_collector
+from . import platform_collector
 
 __all__ = ['Counter', 'Gauge', 'Summary', 'Histogram']
 # http://stackoverflow.com/questions/19913653/no-unicode-in-all-for-a-packages-init
@@ -31,6 +32,9 @@ instance_ip_grouping_key = exposition.instance_ip_grouping_key
 ProcessCollector = process_collector.ProcessCollector
 PROCESS_COLLECTOR = process_collector.PROCESS_COLLECTOR
 
+PlatformCollector = platform_collector.PlatformCollector
+PLATFORM_COLLECTOR = platform_collector.PLATFORM_COLLECTOR
+
 
 if __name__ == '__main__':
     c = Counter('cc', 'A counter')
diff --git a/prometheus_client/platform_collector.py b/prometheus_client/platform_collector.py
new file mode 100644
index 0000000..f70159d
--- /dev/null
+++ b/prometheus_client/platform_collector.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+from __future__ import unicode_literals
+
+import platform as pf
+
+from . import core
+
+
+class PlatformCollector(object):
+    """Collector for python platform information"""
+
+    def __init__(self, registry=core.REGISTRY, platform=None):
+        self._platform = pf if platform is None else platform
+        info = self._info()
+        system = self._platform.system()
+        if system == "Java":
+            info.update(self._java())
+        self._metrics = [
+            self._add_metric("python_info", "Python platform information", info)
+        ]
+        if registry:
+            registry.register(self)
+
+    def collect(self):
+        return self._metrics
+
+    @staticmethod
+    def _add_metric(name, documentation, data):
+        labels = data.keys()
+        values = [data[k] for k in labels]
+        g = core.GaugeMetricFamily(name, documentation, labels=labels)
+        g.add_metric(values, 1)
+        return g
+
+    def _info(self):
+        major, minor, patchlevel = self._platform.python_version_tuple()
+        return {
+            "version": self._platform.python_version(),
+            "implementation": self._platform.python_implementation(),
+            "major": major,
+            "minor": minor,
+            "patchlevel": patchlevel
+        }
+
+    def _java(self):
+        java_version, _, vminfo, osinfo = self._platform.java_ver()
+        vm_name, vm_release, vm_vendor = vminfo
+        return {
+            "jvm_version": java_version,
+            "jvm_release": vm_release,
+            "jvm_vendor": vm_vendor,
+            "jvm_name": vm_name
+        }
+
+
+PLATFORM_COLLECTOR = PlatformCollector()
+"""PlatformCollector in default Registry REGISTRY"""


Test patch:
diff --git a/tests/test_platform_collector.py b/tests/test_platform_collector.py
new file mode 100644
index 0000000..9eda5bb
--- /dev/null
+++ b/tests/test_platform_collector.py
@@ -0,0 +1,69 @@
+from __future__ import unicode_literals
+
+import unittest
+
+from prometheus_client import CollectorRegistry, PlatformCollector
+
+
+class TestPlatformCollector(unittest.TestCase):
+    def setUp(self):
+        self.registry = CollectorRegistry()
+        self.platform = _MockPlatform()
+
+    def test_python_info(self):
+        PlatformCollector(registry=self.registry, platform=self.platform)
+        self.assertLabels("python_info", {
+            "version": "python_version",
+            "implementation": "python_implementation",
+            "major": "pvt_major",
+            "minor": "pvt_minor",
+            "patchlevel": "pvt_patchlevel"
+        })
+
+    def test_system_info_java(self):
+        self.platform._system = "Java"
+        PlatformCollector(registry=self.registry, platform=self.platform)
+        self.assertLabels("python_info", {
+            "version": "python_version",
+            "implementation": "python_implementation",
+            "major": "pvt_major",
+            "minor": "pvt_minor",
+            "patchlevel": "pvt_patchlevel",
+            "jvm_version": "jv_release",
+            "jvm_release": "vm_release",
+            "jvm_vendor": "vm_vendor",
+            "jvm_name": "vm_name"
+        })
+
+    def assertLabels(self, name, labels):
+        for metric in self.registry.collect():
+            for n, l, value in metric.samples:
+                if n == name:
+                    assert l == labels
+                    return
+        assert False
+
+
+class _MockPlatform(object):
+    def __init__(self):
+        self._system = "system"
+
+    def python_version_tuple(self):
+        return "pvt_major", "pvt_minor", "pvt_patchlevel"
+
+    def python_version(self):
+        return "python_version"
+
+    def python_implementation(self):
+        return "python_implementation"
+
+    def system(self):
+        return self._system
+
+    def java_ver(self):
+        return (
+            "jv_release",
+            "jv_vendor",
+            ("vm_name", "vm_release", "vm_vendor"),
+            ("os_name", "os_version", "os_arch")
+        )