From 91dacf3a4557a871b1c4874700e562c3e9e68a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gy=C3=B6rgy=20Krajcsovits?= Date: Thu, 29 May 2025 13:57:00 +0200 Subject: [PATCH] chore(ci): add action to test abnf syntax and examples in OM2.0 spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Action item from OpenMetrics 2.0 WG. Signed-off-by: György Krajcsovits --- .github/workflows/openmetrics.yml | 24 ++++++ docs/specs/om/open_metrics_spec_2_0.md | 2 +- scripts/check_openmetrics_spec.py | 108 +++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/openmetrics.yml create mode 100644 scripts/check_openmetrics_spec.py diff --git a/.github/workflows/openmetrics.yml b/.github/workflows/openmetrics.yml new file mode 100644 index 00000000..41fc46ad --- /dev/null +++ b/.github/workflows/openmetrics.yml @@ -0,0 +1,24 @@ +name: OpenMetrics + +on: + pull_request: + paths: + - 'docs/specs/om/open_metrics_spec_2_0.md' + +jobs: + check-abnf: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up Python 3.x + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12.3" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install abnf + - name: Check ABNF for OpenMetrics 2.0 + run: | + python3 scripts/check_openmetrics_spec.py docs/specs/om/open_metrics_spec_2_0.md diff --git a/docs/specs/om/open_metrics_spec_2_0.md b/docs/specs/om/open_metrics_spec_2_0.md index 9e14b6c2..dfe0ee86 100644 --- a/docs/specs/om/open_metrics_spec_2_0.md +++ b/docs/specs/om/open_metrics_spec_2_0.md @@ -399,7 +399,7 @@ Line endings MUST be signalled with line feed (\n) and MUST NOT contain carriage An example of a complete exposition: -``` +```openmetrics # TYPE acme_http_router_request_seconds summary # UNIT acme_http_router_request_seconds seconds # HELP acme_http_router_request_seconds Latency though all of ACME's HTTP request router. diff --git a/scripts/check_openmetrics_spec.py b/scripts/check_openmetrics_spec.py new file mode 100644 index 00000000..b3d00c4c --- /dev/null +++ b/scripts/check_openmetrics_spec.py @@ -0,0 +1,108 @@ +#!/bin/env python3 +# +# This script opens a markdown file containing the OpenMetrics specification, +# extracts the ABNF grammar from it, and checks if the grammar is valid. +# ABNF grammer must be enclosed in +# ```abnf +# exposition = metricset HASH SP eof [ LF ] +# ... +# ``` +# code block, and the top node must be `exposition`. +# It also extracts examples from the OpenMetrics spec file and checks if they +# are valid according to the grammar. +# Exampes must be enclosed in +# ```openmetrics +# ... example content ... +# ``` +# code blocks. + +from abnf import Rule +import sys + +class Grammar(Rule): + pass + +# Start node for the OpenMetrics spec. +start_node = 'exposition' + +def get_spec(filename): + with open(filename, 'r') as file: + lines = file.readlines() + spec = [] + collecting = False + for line in lines: + if collecting: + if line.startswith('```'): + collecting = False + else: + spec.append(line.strip()) + continue + if line.startswith('```abnf'): + if len(spec) > 0: + raise ValueError("Multiple ABNF blocks found in the file.") + collecting = True + + if len(spec) == 0: + raise ValueError("No or empty ABNF block found in the file. Wanted ```abnf ... ```.") + return '\n'.join(spec) + + +class example: + def __init__(self, line_number, content): + self.line_number = line_number + self.content = content + +class examples: + """ + Extracts examples from the OpenMetrics spec file with generator function. + """ + def __init__(self, filename): + self.file = open(filename, 'r') + self.line_number = 0 + + def __iter__(self): + return self + + def __next__(self): + collecting = False + start_line = self.line_number + example_lines = [] + for line in self.file: + self.line_number += 1 + if collecting: + if line.startswith('```'): + collecting = False + break + else: + example_lines.append(line) + elif line.startswith('```openmetrics'): + start_line = self.line_number + collecting = True + if len(example_lines) > 0: + return example(start_line, ''.join(example_lines).strip()) + + raise StopIteration("No more examples found.") + +# Main +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python3 check_openmetrics_spec.py ") + sys.exit(1) + + filename = sys.argv[1] + if not filename.endswith('.md'): + print(f"Error: {filename} is not a Markdown file.") + sys.exit(1) + spec = get_spec(filename) + try: + Grammar.load_grammar(grammar=spec, strict=True) + except Exception as e: + print(f"Error parsing ABNF: {e}") + sys.exit(1) + print("ABNF parsed successfully.") + for ex in examples(filename): + try: + Grammar.get(start_node).parse_all(ex.content) + print(f"Example parsed successfully: {ex.line_number}: {ex.content[:30]}...") # Print first 30 chars + except Exception as e: + print(f"Error parsing example at line {ex.line_number}: {e}\nExample: {ex.content[:30]}...")