From 18bd338d2d7e93dcb44cd5d6c3f35793d626246e Mon Sep 17 00:00:00 2001 From: David Runge Date: Sat, 31 Jul 2021 21:36:19 +0200 Subject: [PATCH 1/3] Read openmetrics of different types arch_release_promotion/release.py: Add `Metric`, `SizeMetric`, `AmountMetric` and |VersionMetric| models, that track metrics with size, amount or version (respectively). Change the `Release` model to use `amount_metrics`, `size_metrics` and `version_metrics` attributes instead of `info`. arch_release_promotion/config.py: Change the `ReleaseConfig` model to track `version_metrics`, `size_metrics` and `amount_metrics` instead of `info_metrics`. arch_release_promotion/files.py: Change `read_metrics_file()` to return a tuple of AmountMetric, SizeMetric and VersionMetric lists and read the provided file only if it exists. arch_release_promotion/cli.py: Change `main()` to correctly initialize instances of `Release` with the different types of metrics. tests/*: Change the tests to match the changes in signature and attributes. --- arch_release_promotion/cli.py | 15 +++-- arch_release_promotion/config.py | 15 ++++- arch_release_promotion/files.py | 94 +++++++++++++++++++++++-------- arch_release_promotion/release.py | 62 ++++++++++++++++++-- tests/test_config.py | 4 +- tests/test_files.py | 60 ++++++++++++++++---- tests/test_release.py | 35 +++++++++++- 7 files changed, 233 insertions(+), 52 deletions(-) diff --git a/arch_release_promotion/cli.py b/arch_release_promotion/cli.py index a4bb6c4..ba3f7b3 100644 --- a/arch_release_promotion/cli.py +++ b/arch_release_promotion/cli.py @@ -55,16 +55,19 @@ def main() -> None: file_extensions=release_config.extensions_to_sign, ) + metrics = files.read_metrics_file( + path=metrics_file, + version_metrics_names=release_config.version_metrics, + size_metrics_names=release_config.size_metrics, + amount_metrics_names=release_config.amount_metrics, + ) artifact_release = release.Release( name=release_config.name, version=release_version, files=files.files_in_dir(path=artifact_full_path), - info=files.read_metrics_file( - path=metrics_file, - metrics=release_config.info_metrics, - ) - if metrics_file.exists() - else None, + amount_metrics=metrics[0], + size_metrics=metrics[1], + version_metrics=metrics[2], torrent_file=torrent.create_torrent_file( path=artifact_full_path, webseeds=torrent.get_webseeds( diff --git a/arch_release_promotion/config.py b/arch_release_promotion/config.py index e78616a..36c2227 100644 --- a/arch_release_promotion/config.py +++ b/arch_release_promotion/config.py @@ -27,8 +27,15 @@ class ReleaseConfig(BaseModel): ---------- name: str The name of the release (type) - info_metrics: List[str] - A list of openmetrics "name" labels of type info, that should be extracted from the project's metrics file + version_metrics: Optional[List[str]] + A list of names that identify labels in metric samples of type "info", that should be extracted from the + project's metrics file + size_metrics: Optional[List[str]] + A list of names that identify labels in metric samples of type "gauge", that should be extracted from the + project's metrics file + amount_metrics: Optional[List[str]] + A list of names that identify labels in metric samples of type "summary", that should be extracted from the + project's metrics file extensions_to_sign: List[str] A list of file extensions for which to create detached signatures create_torrent: bool @@ -36,7 +43,9 @@ class ReleaseConfig(BaseModel): """ name: str - info_metrics: List[str] + version_metrics: Optional[List[str]] + size_metrics: Optional[List[str]] + amount_metrics: Optional[List[str]] extensions_to_sign: List[str] create_torrent: bool = False diff --git a/arch_release_promotion/files.py b/arch_release_promotion/files.py index abcdd9d..80990d9 100644 --- a/arch_release_promotion/files.py +++ b/arch_release_promotion/files.py @@ -2,12 +2,17 @@ import shutil import tempfile import zipfile from pathlib import Path -from typing import Dict, List +from typing import List, Optional, Tuple import orjson from prometheus_client.parser import text_fd_to_metric_families -from arch_release_promotion.release import Release +from arch_release_promotion.release import ( + AmountMetric, + Release, + SizeMetric, + VersionMetric, +) TEMP_DIR_PREFIX = "arp-" @@ -193,41 +198,82 @@ def write_zip_file_to_parent_dir(path: Path, name: str = "promotion", format: st shutil.make_archive(base_name=str(path.parent / Path(name)), format=format, root_dir=path) -def read_metrics_file(path: Path, metrics: List[str]) -> Dict[str, Dict[str, str]]: - """Read a metrics file that contains openmetrics based metrics and return metrics that match the keywords +def read_metrics_file( + path: Path, + version_metrics_names: Optional[List[str]], + size_metrics_names: Optional[List[str]], + amount_metrics_names: Optional[List[str]], +) -> Tuple[List[AmountMetric], List[SizeMetric], List[VersionMetric]]: + """Read a metrics file that contains openmetrics based metrics and return those that match the respective keywords Parameters ---------- path: Path The path of the file to read - metrics: List[str] - A list of metric names to search for in the metrics file + version_metrics_names: Optional[List[str]] + A list of metric names to search for in the labels of metric samples of type "info" + size_metrics_names: Optional[List[str]], + A list of metric names to search for in the labels of metric samples of type "gauge" + amount_metrics_names: Optional[List[str]], + A list of metric names to search for in the labels of metric samples of type "summary" Returns ------- - Dict[str, str]: - A dictionary representing packages, their respective description and their version + Tuple[List[AmountMetric], List[SizeMetric], List[VersionMetric]]: + A Tuple with lists of AmountMetric, SizeMetric and VersionMetric instances derived from the input file """ - output: Dict[str, Dict[str, str]] = {} + amount_metrics: List[AmountMetric] = [] + size_metrics: List[SizeMetric] = [] + version_metrics: List[VersionMetric] = [] - with open(path, "r") as file: - for metric in text_fd_to_metric_families(file): - if metric.name == "version_info" and metric.samples: + if path.exists(): + with open(path, "r") as file: + for metric in text_fd_to_metric_families(file): for sample in metric.samples: if ( - sample.labels.get("package") + version_metrics_names + and metric.type == "info" + and metric.name == "version_info" + and sample.labels.get("name") in version_metrics_names and sample.labels.get("description") and sample.labels.get("version") - and sample.labels.get("package") in metrics ): - output.update( - { - sample.labels.get("package"): { - "description": sample.labels.get("description"), - "version": sample.labels.get("version"), - } - } - ) - - return output + version_metrics += [ + VersionMetric( + name=sample.labels.get("name"), + description=sample.labels.get("description"), + version=sample.labels.get("version"), + ) + ] + if ( + size_metrics_names + and metric.type == "gauge" + and metric.name == "artifact_bytes" + and sample.labels.get("name") in size_metrics_names + and sample.labels.get("description") + and sample.value + ): + size_metrics += [ + SizeMetric( + name=sample.labels.get("name"), + description=sample.labels.get("description"), + size=sample.value, + ) + ] + if ( + amount_metrics_names + and metric.type == "summary" + and metric.name == "data_count" + and sample.labels.get("name") in amount_metrics_names + and sample.labels.get("description") + and sample.value + ): + amount_metrics += [ + AmountMetric( + name=sample.labels.get("name"), + description=sample.labels.get("description"), + amount=sample.value, + ) + ] + return (amount_metrics, size_metrics, version_metrics) diff --git a/arch_release_promotion/release.py b/arch_release_promotion/release.py index 4e8926c..1cde393 100644 --- a/arch_release_promotion/release.py +++ b/arch_release_promotion/release.py @@ -1,8 +1,56 @@ -from typing import Dict, List, Optional +from typing import List, Optional from pydantic import BaseModel +class Metric(BaseModel): + """A pydantic model describing a metric + + Attributes + ---------- + name: str + A name for the metric + description: str + A description for the metric + """ + + name: str + description: str + + +class SizeMetric(Metric): + """A pydantic model describing a size metric + + Attributes + ---------- + size: int + """ + + size: int + + +class AmountMetric(Metric): + """A pydantic model describing an amount metric + + Attributes + ---------- + amount: int + """ + + amount: int + + +class VersionMetric(Metric): + """A pydantic model describing a version metric + + Attributes + ---------- + version: str + """ + + version: str + + class Release(BaseModel): """A pydantic model describing a release @@ -14,8 +62,12 @@ class Release(BaseModel): The version of the artifact files: List[str] A list of files that belong to the release - info: Dict[str, str] - A dictionary that provides additional information about the release + amount_metrics: List[AmountMetric] + A list of AmountMetric instances that are related to the release + size_metrics: List[SizeMetric] + A list of SizeMetric instances that are related to the release + version_metrics: List[VersionMetric] + A list of VersionMetric instances that are related to the release torrent_file: str A string representing the name of a torrent file for the release developer: str @@ -27,7 +79,9 @@ class Release(BaseModel): name: str version: str files: List[str] - info: Optional[Dict[str, Dict[str, str]]] + amount_metrics: List[AmountMetric] + size_metrics: List[SizeMetric] + version_metrics: List[VersionMetric] torrent_file: Optional[str] developer: str pgp_public_key: str diff --git a/tests/test_config.py b/tests/test_config.py index 5f4c5da..1894e75 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -88,7 +88,7 @@ def test_settings( 'job_name = "build"', 'metrics_file = "metrics.txt"', 'output_dir = "output"', - 'releases = [{name = "test",info_metrics = ["bar"],extensions_to_sign = [".baz"]}]', + 'releases = [{name = "test",version_metrics = ["bar"],extensions_to_sign = [".baz"]}]', ], "foo/bar", does_not_raise(), @@ -101,7 +101,7 @@ def test_settings( 'job_name = "build"', 'metrics_file = "metrics.txt"', 'output_dir = "output"', - 'releases = [{name = "test",info_metrics = ["bar"],extensions_to_sign = [".baz"]}]', + 'releases = [{name = "test",version_metrics = ["bar"],extensions_to_sign = [".baz"]}]', ], "foo/baz", raises(RuntimeError), diff --git a/tests/test_files.py b/tests/test_files.py index 1101e5c..b45899f 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -39,10 +39,20 @@ def create_temp_dir_with_files() -> Iterator[Path]: def create_temp_metrics_file() -> Iterator[Path]: with tempfile.TemporaryDirectory() as temp_dir: with tempfile.NamedTemporaryFile(dir=temp_dir, delete=False) as temp_file: - temp_file.write(b'version_info{package="foo", description="Version of foo", version="1.0.0-1"} 1\n') - temp_file.write(b'version_info{package="foo", not_description="Version of foo", version="1.0.0-1"} 1\n') + temp_file.write(b"# TYPE version_info info\n") + temp_file.write(b"# HELP version_info Package description and version information\n") + temp_file.write(b'version_info{name="foo", description="Version of foo", version="1.0.0-1"} 1\n') + temp_file.write(b'version_info{name="bar", not_description="Version of bar", version="1.0.0-1"} 1\n') temp_file.write(b"version_info 1\n") - temp_file.write(b'foo{package="foo", description="Version of foo", version="1.0.0-1"} 1\n') + temp_file.write(b'version{name="foo", description="Version of foo", version="1.0.0-1"} 1\n') + temp_file.write(b"# TYPE artifact_bytes gauge\n") + temp_file.write(b"# HELP artifact_bytes Artifact sizes in bytes\n") + temp_file.write(b'artifact_bytes{name="foo",description="Size of ISO image in MiB"} 832\n') + temp_file.write(b'artifact_bytes{not_name="foo",description="Size of ISO image in MiB"} 832\n') + temp_file.write(b"# TYPE data_count summary\n") + temp_file.write(b"# HELP data_count The amount of packages used in specific buildmodes\n") + temp_file.write(b'data_count{name="foo",description="The amount of packages in foo"} 369\n') + temp_file.write(b'data_count{not_name="netboot",description="something else"} 369\n') yield Path(temp_file.name) @@ -132,6 +142,9 @@ def test_write_release_info_to_file(create_temp_dir: Path) -> None: name="foo", version="1.0.0", files=["foo", "bar", "baz"], + amount_metrics=[], + size_metrics=[], + version_metrics=[], developer="Foobar McFoo", torrent_file="foo-0.1.0.torrent", pgp_public_key="SOMEONESKEY", @@ -145,6 +158,9 @@ def test_write_release_info_to_file(create_temp_dir: Path) -> None: name="foo", version="1.0.0", files=["foo", "bar", "baz"], + amount_metrics=[], + size_metrics=[], + version_metrics=[], developer="Foobar McFoo", torrent_file="foo-0.1.0.torrent", pgp_public_key="SOMEONESKEY", @@ -176,13 +192,33 @@ def test_write_zip_file_to_parent_dir( assert (create_temp_dir_with_files.parent / Path(f"{name}.zip")).is_file() -@mark.parametrize("metrics", [([]), (["foo"])]) -def test_read_metrics_file(metrics: List[str], create_temp_metrics_file: Path) -> None: - files.read_metrics_file( - path=create_temp_metrics_file, - metrics=metrics, - ) - files.read_metrics_file( - path=create_temp_metrics_file, - metrics=metrics, +@mark.parametrize( + "file_exists, version_metrics_names, size_metrics_names, amount_metrics_names", + [ + (True, [], [], []), + (False, [], [], []), + (True, ["foo"], ["foo"], ["foo"]), + (True, ["bar"], ["foo"], ["foo"]), + (True, ["bar"], ["foo"], ["bar"]), + (True, ["bar"], ["bar"], ["foo"]), + ], +) +def test_read_metrics_file( + file_exists: bool, + version_metrics_names: List[str], + size_metrics_names: List[str], + amount_metrics_names: List[str], + create_temp_metrics_file: Path, +) -> None: + metrics = files.read_metrics_file( + path=create_temp_metrics_file if file_exists else Path("foo"), + version_metrics_names=version_metrics_names, + size_metrics_names=size_metrics_names, + amount_metrics_names=amount_metrics_names, ) + if version_metrics_names == "foo": + assert len(metrics[2]) == 1 + if size_metrics_names == "foo": + assert len(metrics[1]) == 1 + if amount_metrics_names == "foo": + assert len(metrics[0]) == 1 diff --git a/tests/test_release.py b/tests/test_release.py index 267508c..c36a083 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -1,12 +1,45 @@ from arch_release_promotion import release +def test_metric() -> None: + assert release.Metric( + name="foo", + description="bar", + ) + + +def test_amount_metric() -> None: + assert release.AmountMetric( + name="foo", + description="bar", + amount=1, + ) + + +def test_size_metric() -> None: + assert release.SizeMetric( + name="foo", + description="bar", + size=1.1, + ) + + +def test_version_metric() -> None: + assert release.VersionMetric( + name="foo", + description="bar", + version="1.0.0-1", + ) + + def test_release() -> None: assert release.Release( name="foo", version="0.1.0", files=["foo", "bar", "baz"], - info={"bar": {"description": "Version of bar when building foo", "version": "1.0.0"}}, + amount_metrics=[], + size_metrics=[], + version_metrics=[], developer="Foobar McFoo", torrent_file="foo-0.1.0.torrent", pgp_public_key="SOMEONESKEY", -- GitLab From b8726c3aae838a9fcc2cd4e76c4c903e8cad61c8 Mon Sep 17 00:00:00 2001 From: David Runge Date: Sat, 31 Jul 2021 21:46:38 +0200 Subject: [PATCH 2/3] examples: Change according to new metrics behavior examples/projects.toml: Change the example configuration to make use of `version_metrics` instead of `info_metrics`. --- examples/projects.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/projects.toml b/examples/projects.toml index 754d363..828a239 100644 --- a/examples/projects.toml +++ b/examples/projects.toml @@ -12,7 +12,7 @@ releases = [ }, { name = "ipxe", - info_metrics = [ + version_metrics = [ "archiso", "ipxe", ], @@ -20,7 +20,7 @@ releases = [ }, { name = "iso", - info_metrics = [ + version_metrics = [ "archiso", "linux", ], @@ -29,7 +29,7 @@ releases = [ }, { name = "netboot", - info_metrics = [ + version_metrics = [ "archiso", "linux", ], -- GitLab From 662db254fd92db4e172cf4d7a14ee3d2d352c302 Mon Sep 17 00:00:00 2001 From: David Runge Date: Sat, 31 Jul 2021 21:48:25 +0200 Subject: [PATCH 3/3] Add documentation on openmetrics README.rst: Add a section on how and which types of openmetrics are considered to collect version, size, or amount information from a metrics file. Change the JSON payload according to the more diverse set of understood openmetrics. --- README.rst | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 1abfb45..68be650 100644 --- a/README.rst +++ b/README.rst @@ -59,6 +59,102 @@ Use After installation, refer to the output of ``arch-release-promotion -h``. +Openmetrics +=========== + +If the upstream project offers an `openmetrics `_ +based metrics file, the data from it can be used as additional information in +the JSON payload. + +The following metrics are considered. + +Version metrics +--------------- + +Description and version information about e.g. packages can be derived from +``version_info`` metrics of type ``info``, that define a ``name``, +``description`` and ``version`` label. + +For the metrics to be considered, they have to be configured by adding a +``version_metrics`` list (a list of names to look for) to a release of a +project. + +.. code:: + + # TYPE version_info info + # HELP version_info Package description and version information + version_info{name="my-package",description="Version of my-package used for build",version="1.0.0-1"} 1 + +The above metrics entry would result in the following JSON representation: + +.. code:: json + + "version_metrics": [ + { + "name": "my-package", + "description": "Version of my-package used for build", + "version": "1.0.0-1" + } + ] + +Size metrics +------------ + +Artifact size information in MebiBytes (MiB) and description can be derived +from ``artifact_bytes`` metrics of type ``gauge``, that define a ``name`` and a +``description`` label. + +For the metrics to be considered, they have to be configured by adding a +``size_metrics`` list (a list of names to look for) to a release of a +project. + +.. code:: + + # TYPE artifact_bytes gauge + # HELP artifact_bytes Artifact sizes in Bytes + artifact_bytes{name="foo",description="Size of foo in MiB"} 832 + +The above metrics entry would result in the following JSON representation: + +.. code:: json + + "size_metrics": [ + { + "name": "foo", + "description": "Size of foo in MiB", + "size": 832 + } + ] + +Amount metrics +-------------- + +Information on the amount of something (e.g. packages) and description can be +derived from ``data_count`` metrics of type ``summary``, that define a ``name`` +and a ``description`` label. + +For the metrics to be considered, they have to be configured by adding a +``amount_metrics`` list (a list of names to look for) to a release of a +project. + +.. code:: + + # TYPE data_count summary + # HELP data_count The amount of something used in some context + data_count{name="foo",description="The amount of packages in foo"} 369 + +The above metrics entry would result in the following JSON representation: + +.. code:: json + + "amount_metrics": [ + { + "name": "foo", + "description": "The amount of packages in foo", + "amount": 369 + } + ] + JSON payload ============ @@ -70,11 +166,27 @@ each release type in the release. { "developer": "Foobar McFooface ", "files": ["something.txt", "something.txt.sig"], - "info": [ + "version_metrics": [ + { + "my-package": { + "description": "Version of my-package used for build", + "version": "1.0.0-1" + } + } + ], + "size_metrics": [ + { + "foo": { + "description": "Size of foo in MiB", + "size": 832 + } + } + ], + "amount_metrics": [ { - "bar": { - "description": "Version of bar", - "version": "0.3.0" + "foo": { + "description": "The amount of packages in foo", + "size": 369 } } ], -- GitLab