Skip to content

Commit eef9b80

Browse files
committed
Add metrics to deliverorderer
Signed-off-by: Liran Funaro <liran.funaro@gmail.com>
1 parent 2b0e144 commit eef9b80

24 files changed

Lines changed: 867 additions & 325 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ test/**/*.log
4343

4444
.DS_Store
4545
venv
46+
**/__pycache__
4647

4748
# MkDocs build output
4849
site/

docs/metrics_reference.md

Lines changed: 57 additions & 35 deletions
Large diffs are not rendered by default.

scripts/extract_metrics.awk

Lines changed: 0 additions & 102 deletions
This file was deleted.

scripts/metrics_doc.sh

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,9 @@
88
# ./metrics_doc.sh generate - Generate metrics documentation
99
# ./metrics_doc.sh check - Check if documentation is up to date
1010

11-
fabricx_dir="$(cd "$(dirname "$0")/.." && pwd)"
12-
scripts_dir="${fabricx_dir}/scripts"
13-
metrics_doc="${fabricx_dir}/docs/metrics_reference.md"
14-
15-
# extract_metrics - Parses a Go metrics file and outputs markdown table rows.
16-
extract_metrics() {
17-
local filepath="$1"
18-
19-
if [[ ! -f "$filepath" ]]; then
20-
echo "Warning: $filepath not found" >&2
21-
return
22-
fi
23-
24-
# Join concatenated strings (" + " on same line or across lines) and extract metrics
25-
perl -0777 -pe 's/" \+\n\s*"//g; s/" \+ "//g' "$filepath" | awk -f "${scripts_dir}/extract_metrics.awk"
26-
}
11+
repo_root_dir="$(cd "$(dirname "$0")/.." && pwd)"
12+
extract_metrics_script="${repo_root_dir}/scripts/metrics_doc_extract.py"
13+
metrics_doc="${repo_root_dir}/docs/metrics_reference.md"
2714

2815
generate_service_doc() {
2916
local service_name="$1"
@@ -37,7 +24,8 @@ The following ${service_name} metrics are exported for consumption by Prometheus
3724
| Name | Type | Labels | Description |
3825
| ---- | ---- | ------ | ----------- |
3926
EOF
40-
extract_metrics "${fabricx_dir}/${metrics_file}"
27+
# Parses a Go metrics file and outputs markdown table rows.
28+
python3 "${extract_metrics_script}" "${repo_root_dir}/${metrics_file}"
4129
echo ""
4230
}
4331

scripts/metrics_doc_extract.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright IBM Corp All Rights Reserved.
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
"""
8+
Extracts Prometheus metric definitions from Go source files.
9+
10+
This script parses Go files containing Prometheus metric definitions and outputs
11+
Markdown table rows with: Name, Type, Labels, and Description.
12+
13+
It also follows sub-method calls by parsing their implementations.
14+
"""
15+
16+
import re
17+
import sys
18+
from pathlib import Path
19+
20+
repo_root_dir = Path(__file__).parent
21+
22+
# Find the root directory (contains go.mod)
23+
while repo_root_dir.parent != repo_root_dir:
24+
if (repo_root_dir / "go.mod").exists():
25+
break
26+
repo_root_dir = repo_root_dir.parent
27+
28+
# Pattern: New(Counter|Gauge|Histogram)(Vec)?(prometheus.(Counter|Gauge|Histogram)Opts
29+
# Captures: group(1)=metric type, group(2)=Vec or None, group(3)=opts type
30+
metric_pattern = re.compile(r'New([A-Z][a-z]+)(Vec)?\(prometheus\.([A-Z][a-z]+)Opts')
31+
32+
# Pattern: package.FunctionName(..., monitoring.MetricsParameters
33+
# Captures: group(1)=package, group(2)=method name
34+
params_usage_pattern = re.compile(r'(\w+)\.(\w+)\([^)]+,\s*(?:monitoring\.)?MetricsParameters')
35+
36+
37+
def main():
38+
"""Main entry point."""
39+
if len(sys.argv) < 2:
40+
print("Usage: metrics_doc_extract.py <metrics_file>", file=sys.stderr)
41+
sys.exit(1)
42+
43+
metrics_file = Path(sys.argv[1])
44+
if not metrics_file.exists():
45+
print(f"Warning: {metrics_file} not found", file=sys.stderr)
46+
return
47+
48+
print_all_metrics_from_content(metrics_file.read_text())
49+
50+
51+
def print_all_metrics_from_content(content: str):
52+
"""Extract all metrics from Go source content in order of appearance."""
53+
# Join concatenated strings
54+
content = re.sub(r'"\s*\+\s*"', '', content)
55+
56+
# Collect all matches with their positions, sorted by position to maintain order.
57+
for pos, match_type, *rest in sorted(iter_all_matches(content)):
58+
params_block = extract_block(content[pos:], "()")
59+
if not params_block:
60+
continue
61+
62+
if match_type == 'metric':
63+
metric_type, is_vec = rest
64+
print_single_metric_from_block(metric_type, is_vec, params_block)
65+
continue
66+
67+
# match_type == 'method'
68+
package_name, function_name = rest
69+
70+
# Determine the source file based on the package name
71+
source_file = repo_root_dir / "utils" / package_name / "metrics.go"
72+
if not source_file.exists():
73+
print(f"Warning: {source_file} not found", file=sys.stderr)
74+
continue
75+
76+
# Extract the function body
77+
func_body = extract_function_body(source_file.read_text(), function_name)
78+
if not func_body:
79+
continue
80+
81+
# Substitute params.Namespace and params.Subsystem in the function body
82+
namespace = extract_field(params_block, "Namespace")
83+
func_body = re.sub(r'\bparams\.Namespace\b', f'"{namespace}"', func_body)
84+
subsystem = extract_field(params_block, "Subsystem")
85+
func_body = re.sub(r'\bparams\.Subsystem\b', f'"{subsystem}"', func_body)
86+
87+
# Extract metrics from the substituted function body
88+
print_all_metrics_from_content(func_body)
89+
90+
91+
def iter_all_matches(content: str):
92+
# Find all metrics.
93+
for match in metric_pattern.finditer(content):
94+
metric_type = match.group(1).lower() # Counter, Gauge, or Histogram
95+
is_vec = match.group(2) is not None # Vec or None
96+
opts_type = match.group(3).lower() # Counter, Gauge, or Histogram
97+
98+
# Verify that the metric type matches the opts type
99+
assert metric_type == opts_type
100+
yield match.start(), 'metric', metric_type, is_vec
101+
102+
# Find all function calls that use monitoring.MetricsParameters{}
103+
for match in params_usage_pattern.finditer(content):
104+
package_name = match.group(1)
105+
function_name = match.group(2)
106+
yield match.start(), 'method', package_name, function_name
107+
108+
109+
def print_single_metric_from_block(metric_type: str, is_vec: bool, block: str):
110+
"""Print a metric as a Markdown table row."""
111+
namespace = extract_field(block, "Namespace")
112+
subsystem = extract_field(block, "Subsystem")
113+
name = extract_field(block, "Name")
114+
help_text = extract_field(block, "Help")
115+
labels = extract_labels(block) if is_vec else ""
116+
117+
if not name:
118+
return
119+
120+
metric_name = namespace
121+
if subsystem:
122+
metric_name += f"_{subsystem}"
123+
metric_name += f"_{name}"
124+
125+
print(f"| {metric_name} | {metric_type} | {labels} | {help_text} |")
126+
127+
128+
def extract_field(block: str, field: str) -> str:
129+
"""Extract a quoted string value for a given field name."""
130+
# First try direct quoted string
131+
match = re.search(rf'{field}\s*:\s*"([^"]*)"', block)
132+
if match:
133+
return match.group(1)
134+
135+
# Try fmt.Sprintf pattern - may span multiple lines
136+
# Match the format string and all arguments up to the closing paren
137+
match = re.search(rf'{field}\s*:\s*fmt\.Sprintf\(\s*"([^"]*)",([^)]+)\)', block, re.DOTALL | re.MULTILINE)
138+
if match:
139+
format_str = match.group(1)
140+
args_str = match.group(2)
141+
142+
# Extract the arguments
143+
# Remove trailing whitespace, and quotes.
144+
args = tuple(filter(None, [a.strip().strip('"') for a in args_str.split(",")]))
145+
146+
# Simple substitution for %s
147+
return format_str % args
148+
149+
return ""
150+
151+
152+
def extract_labels(block: str) -> str:
153+
"""Extract label names from a Vec metric definition."""
154+
match = re.search(r'\[]string\{([^}]*)}', block, re.MULTILINE)
155+
if not match:
156+
return ""
157+
158+
labels_str = match.group(1)
159+
# Remove quotes and newlines.
160+
labels_str = re.sub(r'["\n]', '', labels_str)
161+
# Replace commas with spaces.
162+
labels_str = re.sub(r',', ' ', labels_str)
163+
# Remove double spaces.
164+
labels_str = re.sub(r'\s+', ' ', labels_str)
165+
return labels_str.strip()
166+
167+
168+
def extract_function_body(content: str, function_name: str) -> str:
169+
"""Extract the body of a function by name."""
170+
# Pattern: func FunctionName(..., params monitoring.MetricsParameters)
171+
match = re.search(
172+
rf'func\s+{function_name}\s*\(([^)]+,)?\s*params\s+(monitoring\.)?MetricsParameters\)',
173+
content, re.MULTILINE,
174+
)
175+
if not match:
176+
return ""
177+
return extract_block(content[match.end():], "{}")
178+
179+
180+
def extract_block(content: str, braces="{}") -> str:
181+
s, e = 0, len(content)
182+
brace_count = 0
183+
for m in re.finditer(rf'[{braces}]', content, re.MULTILINE):
184+
if m.group() == braces[0]:
185+
if brace_count == 0:
186+
s = m.end()
187+
brace_count += 1
188+
else: # v == braces[1]
189+
brace_count -= 1
190+
if brace_count == 0:
191+
e = m.start()
192+
break
193+
return content[s:e]
194+
195+
196+
if __name__ == "__main__":
197+
main()

0 commit comments

Comments
 (0)