From 941fb192ac744940a7b57129c67b095d60c3c109 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 16 Jul 2025 18:47:45 -0300 Subject: [PATCH 01/21] automation: improve metrics gen --- bin/doc-tools.js | 22 +++++++++++--- cli-utils/generate-cluster-docs.sh | 41 ++++++++++++++++++++++---- docker-compose/25.1/docker-compose.yml | 25 +++++++++++----- tools/metrics/metrics.py | 4 +-- 4 files changed, 73 insertions(+), 19 deletions(-) diff --git a/bin/doc-tools.js b/bin/doc-tools.js index fa321ce..137cffe 100755 --- a/bin/doc-tools.js +++ b/bin/doc-tools.js @@ -394,9 +394,19 @@ const commonOptions = { function runClusterDocs(mode, tag, options) { const script = path.join(__dirname, '../cli-utils/generate-cluster-docs.sh'); const args = [mode, tag, options.dockerRepo, options.consoleTag, options.consoleDockerRepo]; - console.log(`⏳ Running ${script} with arguments: ${args.join(' ')}`); + + console.log(`πŸš€ Starting cluster (${mode}/${tag})...`); + + const startTime = Date.now(); const r = spawnSync('bash', [script, ...args], { stdio: 'inherit', shell: true }); - if (r.status !== 0) process.exit(r.status); + const duration = ((Date.now() - startTime) / 1000).toFixed(1); + + if (r.status !== 0) { + console.error(`❌ Script failed with exit code ${r.status}`); + process.exit(r.status); + } else { + console.log(`βœ… Completed ${mode} docs generation (${duration}s)`); + } } // helper to diff two autogenerated directories @@ -448,6 +458,8 @@ automation ) .option('--diff ', 'Also diff autogenerated metrics from β†’ ') .action((options) => { + console.log(`🎯 Starting metrics docs generation for ${options.tag}`); + verifyMetricsDependencies(); const newTag = options.tag; @@ -456,18 +468,20 @@ automation if (oldTag) { const oldDir = path.join('autogenerated', oldTag, 'metrics'); if (!fs.existsSync(oldDir)) { - console.log(`⏳ Generating metrics docs for old tag ${oldTag}…`); + console.log(`⏳ Generating metrics docs for old tag ${oldTag}...`); runClusterDocs('metrics', oldTag, options); } } - console.log(`⏳ Generating metrics docs for new tag ${newTag}…`); + console.log(`⏳ Generating metrics docs for new tag ${newTag}...`); runClusterDocs('metrics', newTag, options); if (oldTag) { + console.log(`πŸ”„ Diffing ${oldTag} β†’ ${newTag}...`); diffDirs('metrics', oldTag, newTag); } + console.log(`βœ… Metrics docs generation completed!`); process.exit(0); }); diff --git a/cli-utils/generate-cluster-docs.sh b/cli-utils/generate-cluster-docs.sh index 2bfa4ac..5ab1718 100755 --- a/cli-utils/generate-cluster-docs.sh +++ b/cli-utils/generate-cluster-docs.sh @@ -2,6 +2,13 @@ set -euo pipefail IFS=$'\n\t' +# Function to log with timestamp (only for key operations) +log_step() { + echo "[$(date '+%H:%M:%S')] $1" +} + +log_step "πŸš€ Starting cluster setup..." + ############################################################################### # Pre-flight: Ensure Docker is available and running ############################################################################### @@ -15,6 +22,11 @@ if ! docker info &> /dev/null; then exit 1 fi +if ! command -v curl &> /dev/null; then + echo "❌ curl is not installed or not in PATH. Please install curl to continue." + exit 1 +fi + ############################################################################### # Load overrides from an optional .env file in the current directory ############################################################################### @@ -56,20 +68,36 @@ export REDPANDA_CONSOLE_DOCKER_REPO="$CONSOLE_REPO" ############################################################################### # Start Redpanda cluster ############################################################################### +log_step "οΏ½ Starting Redpanda cluster..." "$SCRIPT_DIR/start-cluster.sh" "$TAG" # Wait for the cluster to settle if [[ "$MODE" == "metrics" ]]; then - echo "⏳ Waiting 300 seconds for metrics to be available…" - sleep 300 + log_step "⏳ Waiting for metrics endpoint..." + + # Wait for metrics endpoint to be responsive + timeout=300 + counter=0 + metrics_url="http://localhost:19644/public_metrics/" + + while ! curl -f -s "$metrics_url" > /dev/null 2>&1; do + if [ $counter -ge $timeout ]; then + echo "❌ Metrics endpoint did not become ready within ${timeout}s" + exit 1 + fi + sleep 10 + counter=$((counter + 10)) + done + + log_step "βœ… Metrics endpoint ready" else - echo "⏳ Waiting 30 seconds for cluster to be ready…" sleep 30 fi ############################################################################### # Python virtual environment setup ############################################################################### +log_step "🐍 Setting up Python environment..." "$SCRIPT_DIR/python-venv.sh" \ "$SCRIPT_DIR/venv" \ "$SCRIPT_DIR/../tools/metrics/requirements.txt" @@ -77,6 +105,8 @@ fi ############################################################################### # Run documentation generator ############################################################################### +log_step "πŸ“ Generating $MODE documentation..." + if [[ "$MODE" == "metrics" ]]; then "$SCRIPT_DIR/venv/bin/python" \ "$SCRIPT_DIR/../tools/metrics/metrics.py" "$TAG" @@ -85,11 +115,12 @@ else "$SCRIPT_DIR/../tools/gen-rpk-ascii.py" "$TAG" fi -echo "βœ… $MODE docs generated successfully!" +log_step "βœ… Documentation generated successfully" # Tear down the cluster +log_step "🧹 Cleaning up cluster..." cd "$SCRIPT_DIR"/../docker-compose -docker compose -p "$PROJECT_NAME" down --volumes +docker compose -p "$PROJECT_NAME" down --volumes > /dev/null 2>&1 # Return to the original directory cd "$ORIGINAL_PWD" || exit 1 diff --git a/docker-compose/25.1/docker-compose.yml b/docker-compose/25.1/docker-compose.yml index 0d4352d..1e4db25 100644 --- a/docker-compose/25.1/docker-compose.yml +++ b/docker-compose/25.1/docker-compose.yml @@ -58,6 +58,8 @@ services: depends_on: minio: condition: service_healthy + mc: + condition: service_completed_successfully redpanda-1: command: - redpanda @@ -88,8 +90,12 @@ services: - 29092:29092 - 29644:9644 depends_on: - - redpanda-0 - - minio + redpanda-0: + condition: service_started + minio: + condition: service_healthy + mc: + condition: service_completed_successfully redpanda-2: command: - redpanda @@ -120,8 +126,12 @@ services: - 39092:39092 - 39644:9644 depends_on: - - redpanda-0 - - minio + redpanda-0: + condition: service_started + minio: + condition: service_healthy + mc: + condition: service_completed_successfully #################### # Redpanda Console # #################### @@ -132,8 +142,6 @@ services: - redpanda_network entrypoint: /bin/sh command: -c 'echo "$$CONSOLE_CONFIG_FILE" > /tmp/config.yml && /app/console' - volumes: - - ./config:/tmp/config/ environment: CONFIG_FILEPATH: ${CONFIG_FILEPATH:-/tmp/config.yml} CONSOLE_CONFIG_FILE: | @@ -391,10 +399,11 @@ services: - AWS_REGION=local entrypoint: > /bin/sh -c " - until (/usr/bin/mc config host add minio http://minio:9000 minio redpandaTieredStorage7) do echo '...waiting...' && sleep 1; done; + until (/usr/bin/mc alias set minio http://minio:9000 minio redpandaTieredStorage7) do echo '...waiting...' && sleep 1; done; /usr/bin/mc mb minio/redpanda; /usr/bin/mc policy set public minio/redpanda; - tail -f /dev/null + echo 'MinIO bucket initialization complete'; + exit 0 " catalog: image: tabulario/iceberg-rest diff --git a/tools/metrics/metrics.py b/tools/metrics/metrics.py index c6460a9..a46296e 100644 --- a/tools/metrics/metrics.py +++ b/tools/metrics/metrics.py @@ -159,8 +159,8 @@ def ensure_directory_exists(directory): repo_root = os.getcwd() gen_path = os.path.join(repo_root, "autogenerated") if not os.path.isdir(gen_path): - logging.error(f"autogenerated folder not found at: {gen_path}") - sys.exit(1) + logging.info(f"Creating autogenerated folder at: {gen_path}") + os.makedirs(gen_path, exist_ok=True) # Build the output directory using the already provided tag_modified. output_dir = os.path.join(gen_path, tag_modified, "metrics") From 5fbe9dde48f4e1f9823b162f9888e1c385f2c754 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 16 Jul 2025 19:19:50 -0300 Subject: [PATCH 02/21] convert from CRLF to LF --- cli-utils/generate-cluster-docs.sh | 2 +- cli-utils/install-test-dependencies.sh | 2 +- cli-utils/python-venv.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli-utils/generate-cluster-docs.sh b/cli-utils/generate-cluster-docs.sh index 5ab1718..b5e193a 100755 --- a/cli-utils/generate-cluster-docs.sh +++ b/cli-utils/generate-cluster-docs.sh @@ -4,7 +4,7 @@ IFS=$'\n\t' # Function to log with timestamp (only for key operations) log_step() { - echo "[$(date '+%H:%M:%S')] $1" + echo "[$(date '+%H:%M:%S')] $1" } log_step "πŸš€ Starting cluster setup..." diff --git a/cli-utils/install-test-dependencies.sh b/cli-utils/install-test-dependencies.sh index 0fe1861..6a74dd8 100755 --- a/cli-utils/install-test-dependencies.sh +++ b/cli-utils/install-test-dependencies.sh @@ -13,7 +13,7 @@ install_node() { eval "$(fnm env)" || { echo "Failed to load fnm environment"; exit 1; } fnm install --lts || { echo "Failed to install Node.js"; exit 1; } fnm use --lts || { echo "Failed to use Node.js"; exit 1; } - echo "Node.js version: $(node -v)" + echo "Node.js version: $(node -v)" fi } diff --git a/cli-utils/python-venv.sh b/cli-utils/python-venv.sh index d7d9bac..82ae94f 100755 --- a/cli-utils/python-venv.sh +++ b/cli-utils/python-venv.sh @@ -7,7 +7,7 @@ set -euo pipefail VENV_DIR="${1:-venv}" REQ_FILE="${2:-requirements.txt}" -echo "Recreating Python venv at $VENV_DIR..." +echo "Recreating Python venv at $VENV_DIR..." rm -rf "$VENV_DIR" python3 -m venv "$VENV_DIR" "$VENV_DIR/bin/pip" install --upgrade pip --quiet From f2cb3bec06147e9db8591743cc43386e4513c7cf Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Thu, 17 Jul 2025 11:55:28 -0300 Subject: [PATCH 03/21] change health check to a simple command --- docker-compose/25.1/docker-compose.yml | 2 +- docker-compose/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose/25.1/docker-compose.yml b/docker-compose/25.1/docker-compose.yml index 1e4db25..2a53d80 100644 --- a/docker-compose/25.1/docker-compose.yml +++ b/docker-compose/25.1/docker-compose.yml @@ -51,7 +51,7 @@ services: - 19092:19092 - 19644:9644 healthcheck: - test: ["CMD", "rpk", "cluster", "info", "-X", "user=superuser", "-X", "pass=secretpassword"] + test: ["CMD", "rpk", "version"] interval: 10s timeout: 15s retries: 10 diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 0d4352d..44509ee 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -51,7 +51,7 @@ services: - 19092:19092 - 19644:9644 healthcheck: - test: ["CMD", "rpk", "cluster", "info", "-X", "user=superuser", "-X", "pass=secretpassword"] + test: ["CMD", "rpk", "version"] interval: 10s timeout: 15s retries: 10 From fb21e06155322b41e99d125ac0a991e38e7698fa Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Thu, 17 Jul 2025 11:55:46 -0300 Subject: [PATCH 04/21] check if linux or wsl (path changes) --- bin/doc-tools.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/bin/doc-tools.js b/bin/doc-tools.js index 137cffe..adfc281 100755 --- a/bin/doc-tools.js +++ b/bin/doc-tools.js @@ -397,8 +397,28 @@ function runClusterDocs(mode, tag, options) { console.log(`πŸš€ Starting cluster (${mode}/${tag})...`); + // Detect the correct bash path for cross-platform compatibility + let bashPath = 'bash'; + try { + // Try to get the actual path to bash + const bashLocation = execSync('which bash', { encoding: 'utf8' }).trim(); + if (bashLocation) { + bashPath = bashLocation; + } + } catch { + // Fallback to common paths if 'which' fails + if (fs.existsSync('/usr/bin/bash')) { + bashPath = '/usr/bin/bash'; + } else if (fs.existsSync('/bin/bash')) { + bashPath = '/bin/bash'; + } + } + + console.log(`πŸ”§ Using bash at: ${bashPath}`); + const startTime = Date.now(); - const r = spawnSync('bash', [script, ...args], { stdio: 'inherit', shell: true }); + // Run bash with explicit options to ensure it behaves correctly + const r = spawnSync(bashPath, ['--', script, ...args], { stdio: 'inherit' }); const duration = ((Date.now() - startTime) / 1000).toFixed(1); if (r.status !== 0) { From 1f8ca7a2858fd876dcf1bc6a94ddcbd19c8ed3dd Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Fri, 18 Jul 2025 18:02:57 -0300 Subject: [PATCH 05/21] modify metrics extraction --- tools/metrics/compare_metrics.py | 365 +++++++++++++++++++++++++++++++ tools/metrics/metrics.py | 31 ++- 2 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 tools/metrics/compare_metrics.py diff --git a/tools/metrics/compare_metrics.py b/tools/metrics/compare_metrics.py new file mode 100644 index 0000000..8e06c8d --- /dev/null +++ b/tools/metrics/compare_metrics.py @@ -0,0 +1,365 @@ +#!/usr/bin/env python3 +""" +AsciiDoc Metrics Comparison Tool + +This script compares two AsciiDoc files containing Redpanda metrics documentation. +It extracts metric information from both files and provides detailed comparison results. +Handles different heading levels (== vs ===) for the same metrics. +""" + +import re +import argparse +from typing import Dict, List, Tuple, Optional +from dataclasses import dataclass +from difflib import SequenceMatcher + + +@dataclass +class Metric: + """Represents a single metric with its properties.""" + name: str + description: str + type_info: str + labels: List[str] + usage: str + related_topics: List[str] + raw_content: str + heading_level: str # Added to track original heading level + + +class MetricsParser: + """Parser for extracting metrics from AsciiDoc files.""" + + def __init__(self): + # Updated pattern to match both == and === metric sections + # This pattern captures metrics that start with redpanda_, vectorized_, or similar prefixes + self.metric_pattern = re.compile( + r'^(={2,3})\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\n\n(.*?)(?=\n={2,3}|\n=(?!=)|\Z)', + re.DOTALL | re.MULTILINE + ) + + def parse_file(self, content: str) -> Dict[str, Metric]: + """Parse AsciiDoc content and extract metrics.""" + metrics = {} + + matches = self.metric_pattern.findall(content) + + for match in matches: + heading_level = match[0] # == or === + metric_name = match[1].strip() + metric_content = match[2].strip() + + # Only process if it looks like a metric name (contains underscore and doesn't start with uppercase) + if '_' in metric_name and not metric_name[0].isupper(): + try: + metric = self._parse_metric_content(metric_name, metric_content, heading_level) + metrics[metric_name] = metric + except Exception as e: + print(f"Warning: Failed to parse metric {metric_name}: {e}") + + return metrics + + def _parse_metric_content(self, name: str, content: str, heading_level: str) -> Metric: + """Parse individual metric content.""" + lines = content.split('\n') + + # Extract description (first non-empty line before *Type*) + description = "" + type_info = "" + labels = [] + usage = "" + related_topics = [] + + i = 0 + # Get description + while i < len(lines): + line = lines[i].strip() + if line and not line.startswith('*Type*'): + description = line + break + i += 1 + + # Extract other fields + current_section = None + section_content = [] + + for line in lines: + line = line.strip() + + if line.startswith('*Type*:'): + if current_section: + self._process_section(current_section, section_content, locals()) + current_section = 'type' + section_content = [line.replace('*Type*:', '').strip()] + + elif line.startswith('*Labels*:'): + if current_section: + self._process_section(current_section, section_content, locals()) + current_section = 'labels' + section_content = [] + + elif line.startswith('*Usage*:'): + if current_section: + self._process_section(current_section, section_content, locals()) + current_section = 'usage' + section_content = [] + + elif line.startswith('*Related topics*:'): + if current_section: + self._process_section(current_section, section_content, locals()) + current_section = 'related' + section_content = [] + + elif line.startswith('---'): + if current_section: + self._process_section(current_section, section_content, locals()) + break + + elif current_section and line: + section_content.append(line) + + # Process final section + if current_section: + self._process_section(current_section, section_content, locals()) + + return Metric( + name=name, + description=description, + type_info=type_info, + labels=labels, + usage=usage, + related_topics=related_topics, + raw_content=content, + heading_level=heading_level + ) + + def _process_section(self, section: str, content: List[str], local_vars: dict): + """Process content for specific sections.""" + if section == 'type': + local_vars['type_info'] = ' '.join(content).strip() + elif section == 'labels': + # Extract labels, handling various formats + for line in content: + if line.startswith('*') or line.startswith('-'): + # Remove markdown formatting and extract label + clean_line = re.sub(r'[*`-]', '', line).strip() + if clean_line: + local_vars['labels'].append(clean_line) + elif section == 'usage': + local_vars['usage'] = ' '.join(content).strip() + elif section == 'related': + local_vars['related_topics'] = content.copy() + + +class MetricsComparator: + """Compares two sets of metrics and provides detailed analysis.""" + + def __init__(self): + self.similarity_threshold = 0.8 + + def compare(self, file1_metrics: Dict[str, Metric], file2_metrics: Dict[str, Metric]) -> dict: + """Compare two sets of metrics and return detailed results.""" + + file1_names = set(file1_metrics.keys()) + file2_names = set(file2_metrics.keys()) + + # Find differences + only_in_file1 = file1_names - file2_names + only_in_file2 = file2_names - file1_names + common_metrics = file1_names & file2_names + + # Analyze common metrics for description improvements + improved_descriptions = [] + different_properties = [] + heading_level_differences = [] + + for metric_name in common_metrics: + metric1 = file1_metrics[metric_name] + metric2 = file2_metrics[metric_name] + + # Check for heading level differences + if metric1.heading_level != metric2.heading_level: + heading_level_differences.append({ + 'name': metric_name, + 'file1_level': metric1.heading_level, + 'file2_level': metric2.heading_level + }) + + # Compare descriptions + if metric1.description != metric2.description: + similarity = self._calculate_similarity(metric1.description, metric2.description) + + improved_descriptions.append({ + 'name': metric_name, + 'file1_desc': metric1.description, + 'file2_desc': metric2.description, + 'similarity': similarity, + 'likely_improvement': len(metric1.description) > len(metric2.description) and similarity > 0.5 + }) + + # Compare other properties + differences = self._compare_metric_properties(metric1, metric2) + if differences: + different_properties.append({ + 'name': metric_name, + 'differences': differences + }) + + return { + 'file1_unique': sorted(only_in_file1), + 'file2_unique': sorted(only_in_file2), + 'common_count': len(common_metrics), + 'improved_descriptions': improved_descriptions, + 'different_properties': different_properties, + 'heading_level_differences': heading_level_differences, + 'total_file1': len(file1_metrics), + 'total_file2': len(file2_metrics) + } + + def _calculate_similarity(self, text1: str, text2: str) -> float: + """Calculate similarity between two text strings.""" + return SequenceMatcher(None, text1.lower(), text2.lower()).ratio() + + def _compare_metric_properties(self, metric1: Metric, metric2: Metric) -> List[str]: + """Compare properties of two metrics and return list of differences.""" + differences = [] + + if metric1.type_info != metric2.type_info: + differences.append(f"Type: '{metric1.type_info}' vs '{metric2.type_info}'") + + if set(metric1.labels) != set(metric2.labels): + differences.append(f"Labels differ") + + if metric1.usage != metric2.usage: + differences.append(f"Usage differs") + + return differences + + +def print_comparison_results(results: dict, file1_name: str, file2_name: str): + """Print detailed comparison results.""" + + print(f"\n{'='*60}") + print(f"METRICS COMPARISON REPORT") + print(f"{'='*60}") + print(f"File 1 ({file1_name}): {results['total_file1']} metrics") + print(f"File 2 ({file2_name}): {results['total_file2']} metrics") + print(f"Common metrics: {results['common_count']}") + + # Heading level differences + if results['heading_level_differences']: + print(f"\nπŸ“ HEADING LEVEL DIFFERENCES:") + print(f" Count: {len(results['heading_level_differences'])}") + for item in results['heading_level_differences']: + print(f" - {item['name']}: {item['file1_level']} vs {item['file2_level']}") + + # Metrics only in file 1 (should be removed) + if results['file1_unique']: + print(f"\nπŸ—‘οΈ METRICS TO REMOVE (only in {file1_name}):") + print(f" Count: {len(results['file1_unique'])}") + for metric in results['file1_unique']: + print(f" - {metric}") + + # Metrics only in file 2 (missing from file 1) + if results['file2_unique']: + print(f"\nπŸ“ METRICS MISSING FROM {file1_name}:") + print(f" Count: {len(results['file2_unique'])}") + for metric in results['file2_unique']: + print(f" - {metric}") + + # Description improvements + if results['improved_descriptions']: + print(f"\n✨ POTENTIAL DESCRIPTION IMPROVEMENTS:") + print(f" Count: {len(results['improved_descriptions'])}") + + for item in results['improved_descriptions']: + print(f"\n πŸ“Š {item['name']}:") + print(f" Similarity: {item['similarity']:.2f}") + + if item['likely_improvement']: + print(f" πŸ” LIKELY IMPROVEMENT (File 1 has longer description)") + + print(f" File 1: {item['file1_desc'][:100]}{'...' if len(item['file1_desc']) > 100 else ''}") + print(f" File 2: {item['file2_desc'][:100]}{'...' if len(item['file2_desc']) > 100 else ''}") + + # Other property differences + if results['different_properties']: + print(f"\nπŸ”§ OTHER PROPERTY DIFFERENCES:") + print(f" Count: {len(results['different_properties'])}") + + for item in results['different_properties']: + print(f"\n πŸ“Š {item['name']}:") + for diff in item['differences']: + print(f" - {diff}") + + +def main(): + """Main function to run the comparison tool.""" + parser = argparse.ArgumentParser(description='Compare AsciiDoc metrics files') + parser.add_argument('file1', help='First AsciiDoc file (formatted)') + parser.add_argument('file2', help='Second AsciiDoc file (factual)') + parser.add_argument('--output', '-o', help='Output file for results') + parser.add_argument('--debug', action='store_true', help='Enable debug output') + + args = parser.parse_args() + + # Read files + try: + with open(args.file1, 'r', encoding='utf-8') as f: + content1 = f.read() + with open(args.file2, 'r', encoding='utf-8') as f: + content2 = f.read() + except FileNotFoundError as e: + print(f"Error: File not found - {e}") + return 1 + except Exception as e: + print(f"Error reading files: {e}") + return 1 + + # Parse metrics + parser = MetricsParser() + print("Parsing first file...") + metrics1 = parser.parse_file(content1) + print("Parsing second file...") + metrics2 = parser.parse_file(content2) + + if args.debug: + print(f"Debug: Found {len(metrics1)} metrics in file1") + print(f"Debug: Found {len(metrics2)} metrics in file2") + if metrics1: + print(f"Debug: Sample metrics from file1: {list(metrics1.keys())[:5]}") + if metrics2: + print(f"Debug: Sample metrics from file2: {list(metrics2.keys())[:5]}") + + # Compare metrics + comparator = MetricsComparator() + results = comparator.compare(metrics1, metrics2) + + # Print results + print_comparison_results(results, args.file1, args.file2) + + # Save to file if requested + if args.output: + try: + import sys + from io import StringIO + + # Capture output + old_stdout = sys.stdout + sys.stdout = captured_output = StringIO() + print_comparison_results(results, args.file1, args.file2) + sys.stdout = old_stdout + + # Write to file + with open(args.output, 'w', encoding='utf-8') as f: + f.write(captured_output.getvalue()) + + print(f"\nResults saved to: {args.output}") + except Exception as e: + print(f"Error saving output: {e}") + + return 0 + + +if __name__ == '__main__': + exit(main()) \ No newline at end of file diff --git a/tools/metrics/metrics.py b/tools/metrics/metrics.py index a46296e..f9e8bbe 100644 --- a/tools/metrics/metrics.py +++ b/tools/metrics/metrics.py @@ -120,6 +120,27 @@ def parse_metrics(metrics_text): logging.info(f"Extracted {len(metrics)} metrics.") return metrics +def filter_metrics_for_docs(metrics): + """Filter metrics for documentation - remove duplicates and histogram suffixes.""" + filtered = {} + seen_names = set() # Track metric names to detect duplicates + + for name, data in metrics.items(): + # Skip histogram/summary suffixes + if name.endswith(('_bucket', '_count', '_sum')): + continue + + # Check for duplicate metric names + if name in seen_names: + logging.warning(f"Duplicate metric name found: {name}") + continue + + filtered[name] = data + seen_names.add(name) + + logging.info(f"Filtered from {len(metrics)} to {len(filtered)} metrics for documentation.") + return filtered + def output_asciidoc(metrics, adoc_file): """Output metrics as AsciiDoc.""" with open(adoc_file, "w") as f: @@ -183,7 +204,11 @@ def ensure_directory_exists(directory): logging.error("No internal metrics retrieved.") internal_metrics = {} - # Merge public and internal metrics. + # Filter metrics for documentation + public_metrics_filtered = filter_metrics_for_docs(public_metrics) + internal_metrics_filtered = filter_metrics_for_docs(internal_metrics) + + # Merge public and internal metrics (unfiltered for JSON) merged_metrics = { "public": public_metrics, "internal": internal_metrics @@ -195,5 +220,5 @@ def ensure_directory_exists(directory): INTERNAL_ASCIIDOC_OUTPUT_FILE = os.path.join(output_dir, "internal-metrics.adoc") output_json(merged_metrics, JSON_OUTPUT_FILE) - output_asciidoc(public_metrics, ASCIIDOC_OUTPUT_FILE) - output_asciidoc(internal_metrics, INTERNAL_ASCIIDOC_OUTPUT_FILE) + output_asciidoc(public_metrics_filtered, ASCIIDOC_OUTPUT_FILE) + output_asciidoc(internal_metrics_filtered, INTERNAL_ASCIIDOC_OUTPUT_FILE) \ No newline at end of file From 17597be7186fe916287c43b9e2aaf0bc4dad60a7 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Fri, 18 Jul 2025 18:03:10 -0300 Subject: [PATCH 06/21] introduce new metrics automation --- bin/doc-tools.js | 78 +- tools/metrics-extractor/Makefile | 139 + tools/metrics-extractor/README.adoc | 355 ++ tools/metrics-extractor/compare_metrics.py | 186 + tools/metrics-extractor/debug_queries.py | 217 + tools/metrics-extractor/debug_test.json | 287 + tools/metrics-extractor/debug_test2.json | 287 + tools/metrics-extractor/enhanced_test.json | 251 + tools/metrics-extractor/example.py | 202 + tools/metrics-extractor/metrics.json | 5036 +++++++++++++++++ tools/metrics-extractor/metrics_bag.py | 170 + tools/metrics-extractor/metrics_extractor.py | 181 + tools/metrics-extractor/metrics_parser.py | 315 ++ tools/metrics-extractor/requirements.txt | 2 + tools/metrics-extractor/sample.json | 251 + tools/metrics-extractor/test_filtered.json | 10 + .../tests/test_extraction.py | 113 + tools/metrics-extractor/tree-sitter-cpp.so | Bin 0 -> 3555304 bytes tools/metrics-extractor/validate.py | 169 + 19 files changed, 8228 insertions(+), 21 deletions(-) create mode 100644 tools/metrics-extractor/Makefile create mode 100644 tools/metrics-extractor/README.adoc create mode 100644 tools/metrics-extractor/compare_metrics.py create mode 100644 tools/metrics-extractor/debug_queries.py create mode 100644 tools/metrics-extractor/debug_test.json create mode 100644 tools/metrics-extractor/debug_test2.json create mode 100644 tools/metrics-extractor/enhanced_test.json create mode 100644 tools/metrics-extractor/example.py create mode 100644 tools/metrics-extractor/metrics.json create mode 100644 tools/metrics-extractor/metrics_bag.py create mode 100644 tools/metrics-extractor/metrics_extractor.py create mode 100644 tools/metrics-extractor/metrics_parser.py create mode 100644 tools/metrics-extractor/requirements.txt create mode 100644 tools/metrics-extractor/sample.json create mode 100644 tools/metrics-extractor/test_filtered.json create mode 100644 tools/metrics-extractor/tests/test_extraction.py create mode 100644 tools/metrics-extractor/tree-sitter-cpp.so create mode 100644 tools/metrics-extractor/validate.py diff --git a/bin/doc-tools.js b/bin/doc-tools.js index adfc281..aad85eb 100755 --- a/bin/doc-tools.js +++ b/bin/doc-tools.js @@ -264,6 +264,27 @@ function verifyMetricsDependencies() { requireDockerDaemon(); } +/** + * Ensures all dependencies required for generating metrics documentation from source code are installed. + * + * Checks for the presence of `make`, Python 3.10 or newer, Git, and at least one C++ compiler (`gcc` or `clang`). + * Exits the process with an error message if any dependency is missing. + */ +function verifyMetricsExtractorDependencies() { + requireCmd('make', 'Your OS package manager'); + requirePython(); + requireCmd('git', 'Install Git: https://git-scm.com/downloads'); + try { + execSync('gcc --version', { stdio: 'ignore' }); + } catch { + try { + execSync('clang --version', { stdio: 'ignore' }); + } catch { + fail('A C++ compiler (gcc or clang) is required for tree-sitter compilation.'); + } + } +} + // -------------------------------------------------------------------- // Main CLI Definition // -------------------------------------------------------------------- @@ -397,28 +418,8 @@ function runClusterDocs(mode, tag, options) { console.log(`πŸš€ Starting cluster (${mode}/${tag})...`); - // Detect the correct bash path for cross-platform compatibility - let bashPath = 'bash'; - try { - // Try to get the actual path to bash - const bashLocation = execSync('which bash', { encoding: 'utf8' }).trim(); - if (bashLocation) { - bashPath = bashLocation; - } - } catch { - // Fallback to common paths if 'which' fails - if (fs.existsSync('/usr/bin/bash')) { - bashPath = '/usr/bin/bash'; - } else if (fs.existsSync('/bin/bash')) { - bashPath = '/bin/bash'; - } - } - - console.log(`πŸ”§ Using bash at: ${bashPath}`); - const startTime = Date.now(); - // Run bash with explicit options to ensure it behaves correctly - const r = spawnSync(bashPath, ['--', script, ...args], { stdio: 'inherit' }); + const r = spawnSync('bash', [script, ...args], { stdio: 'inherit', shell: true }); const duration = ((Date.now() - startTime) / 1000).toFixed(1); if (r.status !== 0) { @@ -747,6 +748,41 @@ automation process.exit(0); }); +automation + .command('source-metrics-docs') + .description('Generate metrics documentation from Redpanda source code using tree-sitter') + .option('--tag ', 'Git tag or branch to extract from', 'dev') + .option('--diff ', 'Also diff autogenerated metrics from β†’ ') + .action((options) => { + verifyMetricsExtractorDependencies(); + + const newTag = options.tag; + const oldTag = options.diff; + const cwd = path.resolve(__dirname, '../tools/metrics-extractor'); + const make = (tag) => { + console.log(`⏳ Building source-based metrics docs for ${tag}…`); + const r = spawnSync('make', ['build', `TAG=${tag}`], { cwd, stdio: 'inherit' }); + if (r.error) { + console.error(`❌ ${r.error.message}`); + process.exit(1); + } + if (r.status !== 0) process.exit(r.status); + }; + + if (oldTag) { + const oldDir = path.join('autogenerated', oldTag, 'source-metrics'); + if (!fs.existsSync(oldDir)) make(oldTag); + } + + make(newTag); + + if (oldTag) { + diffDirs('source-metrics', oldTag, newTag); + } + + process.exit(0); + }); + automation .command('rpk-docs') .description('Generate AsciiDoc documentation for rpk CLI commands') diff --git a/tools/metrics-extractor/Makefile b/tools/metrics-extractor/Makefile new file mode 100644 index 0000000..05ea988 --- /dev/null +++ b/tools/metrics-extractor/Makefile @@ -0,0 +1,139 @@ +# Redpanda Metrics Extractor Makefile +# Extracts metrics from Redpanda source code using tree-sitter + +SHELL := /bin/bash +TAG ?= dev +OUTPUT_DIR := autogenerated/$(TAG)/source-metrics +REDPANDA_REPO := https://github.com/redpanda-data/redpanda.git +REDPANDA_DIR := tmp/redpanda-$(TAG) +TREESITTER_DIR := tree-sitter/tree-sitter-cpp +PYTHON_VENV := venv +PYTHON := $(PYTHON_VENV)/bin/python +PIP := $(PYTHON_VENV)/bin/pip +TREE_SITTER := npx tree-sitter + +.PHONY: all build clean setup-venv install-deps clone-redpanda extract-metrics help + +all: build + +help: + @echo "Redpanda Metrics Extractor" + @echo "" + @echo "Available targets:" + @echo " build - Extract metrics for specified TAG (default: dev)" + @echo " clean - Clean temporary files and output" + @echo " setup-venv - Set up Python virtual environment" + @echo " install-deps - Install Python dependencies" + @echo " clone-redpanda - Clone Redpanda repository" + @echo " extract-metrics - Run metrics extraction" + @echo "" + @echo "Usage examples:" + @echo " make build TAG=v23.3.1" + @echo " make build TAG=dev" + +build: setup-venv install-deps clone-redpanda treesitter extract-metrics + +setup-venv: + @echo "Setting up Python virtual environment..." + python3 -m venv $(PYTHON_VENV) + +install-deps: setup-venv + @echo "Installing Python dependencies..." + $(PIP) install --upgrade pip + $(PIP) install -r requirements.txt + +clone-redpanda: + @echo "Cloning Redpanda repository (tag: $(TAG))..." + @mkdir -p tmp + @if [ -d "$(REDPANDA_DIR)" ]; then \ + echo "Repository already exists, updating..."; \ + cd $(REDPANDA_DIR) && git fetch --all && git checkout $(TAG) && git pull origin $(TAG); \ + else \ + git clone --depth 1 --branch $(TAG) $(REDPANDA_REPO) $(REDPANDA_DIR); \ + fi + +treesitter: + @echo "Ensuring tree-sitter-cpp grammar..." + @if [ ! -d "$(TREESITTER_DIR)" ]; then \ + git clone https://github.com/tree-sitter/tree-sitter-cpp.git "$(TREESITTER_DIR)"; \ + fi + @echo "Checking out compatible version v0.20.5..." + @cd "$(TREESITTER_DIR)" && git checkout v0.20.5 + @echo "Generating parser in $(TREESITTER_DIR)..." + @cd "$(TREESITTER_DIR)" && npm install --silent && $(TREE_SITTER) generate + +extract-metrics: + @echo "Extracting metrics from Redpanda source code..." + @mkdir -p $(OUTPUT_DIR) + $(PYTHON) metrics_extractor.py \ + --recursive \ + --output $(OUTPUT_DIR)/metrics.json \ + --asciidoc $(OUTPUT_DIR)/metrics.adoc \ + --verbose \ + $(REDPANDA_DIR)/src + +generate-comparison: + @echo "Generating metrics comparison..." + @if [ -f "$(OUTPUT_DIR)/metrics.json" ]; then \ + $(PYTHON) compare_metrics.py $(OUTPUT_DIR)/metrics.json; \ + else \ + echo "No metrics file found. Run 'make build' first."; \ + fi + +clean: + @echo "Cleaning temporary files and output..." + rm -rf tmp/ + rm -rf tree-sitter/ + rm -rf $(PYTHON_VENV)/ + rm -rf autogenerated/ + find . -name "*.pyc" -delete + find . -name "__pycache__" -delete + +clean-cache: + @echo "Cleaning Python cache..." + find . -name "*.pyc" -delete + find . -name "__pycache__" -delete + +install-system-deps: + @echo "Installing system dependencies..." + @echo "Make sure you have the following installed:" + @echo " - Python 3.8+" + @echo " - git" + @echo " - build-essential (on Linux)" + @echo " - tree-sitter CLI (optional)" + +test: + @echo "Running tests..." + $(PYTHON) -m pytest tests/ -v + +lint: + @echo "Running linting..." + $(PYTHON) -m flake8 *.py + $(PYTHON) -m black --check *.py + +format: + @echo "Formatting code..." + $(PYTHON) -m black *.py + +# Development targets +dev-setup: setup-venv install-deps + $(PIP) install pytest flake8 black + +dev-test: dev-setup + make test + +# Quick extraction from local Redpanda directory +extract-local: + @if [ -z "$(REDPANDA_PATH)" ]; then \ + echo "Error: REDPANDA_PATH not set. Usage: make extract-local REDPANDA_PATH=/path/to/redpanda"; \ + exit 1; \ + fi + @echo "Extracting metrics from local Redpanda at $(REDPANDA_PATH)..." + @mkdir -p $(OUTPUT_DIR) + $(PYTHON) metrics_extractor.py \ + --recursive \ + --output $(OUTPUT_DIR)/metrics.json \ + --asciidoc $(OUTPUT_DIR)/metrics.adoc \ + --filter-namespace redpanda \ + --verbose \ + $(REDPANDA_PATH)/src diff --git a/tools/metrics-extractor/README.adoc b/tools/metrics-extractor/README.adoc new file mode 100644 index 0000000..1c71b6d --- /dev/null +++ b/tools/metrics-extractor/README.adoc @@ -0,0 +1,355 @@ += Redpanda Metrics Extractor +:description: Automated extraction of metrics from Redpanda source code using tree-sitter +:page-categories: Development, Documentation, Automation + +This tool automatically extracts Redpanda metrics from C++ source code using tree-sitter parsing. It identifies metrics created with various Seastar and Redpanda metric constructors and generates comprehensive documentation. + +== Overview + +The metrics extractor uses tree-sitter to parse C++ source code and identify metrics created with these constructors: + +* `sm::make_gauge` +* `sm::make_counter` +* `sm::make_histogram` +* `sm::make_total_bytes` +* `sm::make_derive` +* `ss::metrics::make_total_operations` +* `ss::metrics::make_current_bytes` + +For each metric found, it extracts: + +* Metric name +* Type (gauge, counter, histogram) +* Description +* Potential labels +* Source file location +* Constructor used + +== Prerequisites + +* Python 3.8 or higher +* Git +* Build tools (gcc/clang for compiling tree-sitter) + +== Installation + +1. Set up the Python virtual environment: ++ +[source,bash] +---- +make setup-venv +---- + +2. Install dependencies: ++ +[source,bash] +---- +make install-deps +---- + +== Usage + +=== Extract Metrics from Redpanda Repository + +Extract metrics from a specific Redpanda version: + +[source,bash] +---- +make build TAG=v23.3.1 +---- + +Extract metrics from the development branch: + +[source,bash] +---- +make build TAG=dev +---- + +=== Extract Metrics from Local Repository + +If you have a local Redpanda repository: + +[source,bash] +---- +make extract-local REDPANDA_PATH=/path/to/redpanda +---- + +=== Manual Extraction + +Run the extractor directly: + +[source,bash] +---- +python metrics_extractor.py /path/to/redpanda/src \ + --recursive \ + --output metrics.json \ + --asciidoc metrics.adoc \ + --filter-namespace redpanda \ + --verbose +---- + +== Output Files + +The extractor generates several output files: + +=== JSON Output (`metrics.json`) + +Contains structured metric data: + +[source,json] +---- +{ + "metrics": { + "redpanda_kafka_requests_total": { + "name": "redpanda_kafka_requests_total", + "type": "counter", + "description": "Total number of Kafka requests", + "labels": ["request_type", "status"], + "constructor": "make_counter", + "files": [{"file": "src/v/kafka/server/handlers/handler.cc", "line": 45}] + } + }, + "statistics": { + "total_metrics": 150, + "by_type": {"counter": 75, "gauge": 60, "histogram": 15}, + "by_constructor": {"make_counter": 75, "make_gauge": 60, "make_histogram": 15}, + "with_description": 140, + "with_labels": 85 + } +} +---- + +=== AsciiDoc Output (`metrics.adoc`) + +Human-readable documentation in AsciiDoc format: + +[source,asciidoc] +---- +=== redpanda_kafka_requests_total + +Total number of Kafka requests + +*Type*: counter + +*Labels*: + +- `request_type` +- `status` + +*Source*: `src/v/kafka/server/handlers/handler.cc` + +--- +---- + +== Command Line Options + +=== metrics_extractor.py + +[source,bash] +---- +python metrics_extractor.py [OPTIONS] PATH + +Arguments: + PATH Path to Redpanda source directory + +Options: + -r, --recursive Search for C++ files recursively + -o, --output FILE Output JSON file (default: metrics.json) + -a, --asciidoc FILE Generate AsciiDoc output file + --filter-namespace NS Filter metrics by namespace (e.g., redpanda) + --treesitter-dir DIR Tree-sitter directory (default: tree-sitter) + --treesitter-lib FILE Tree-sitter library file + -v, --verbose Enable verbose logging + -h, --help Show help message +---- + +=== compare_metrics.py + +[source,bash] +---- +python compare_metrics.py [OPTIONS] METRICS_FILE + +Arguments: + METRICS_FILE JSON file with extracted metrics + +Options: + --existing FILE Existing metrics JSON for comparison + --output FILE Output comparison report to JSON + -v, --verbose Verbose logging + -h, --help Show help message +---- + +== Integration with Documentation Pipeline + +The metrics extractor integrates with the existing documentation automation: + +=== Adding to doc-tools.js + +Add the metrics extraction command to the CLI: + +[source,javascript] +---- +automation + .command('metric-docs') + .description('Generate metrics documentation from Redpanda source code') + .option('--tag ', 'Git tag or branch to extract from', 'dev') + .action((options) => { + const cwd = path.resolve(__dirname, '../tools/metrics-extractor'); + const make = spawnSync('make', ['build', `TAG=${options.tag}`], + { cwd, stdio: 'inherit' }); + if (make.status !== 0) process.exit(make.status); + }); +---- + +=== Autogenerated Directory Structure + +The extractor follows the same pattern as property-extractor: + +---- +autogenerated/ +β”œβ”€β”€ v23.3.1/ +β”‚ └── metrics/ +β”‚ β”œβ”€β”€ metrics.json +β”‚ └── metrics.adoc +└── dev/ + └── metrics/ + β”œβ”€β”€ metrics.json + └── metrics.adoc +---- + +== Development + +=== Running Tests + +[source,bash] +---- +make test +---- + +=== Code Formatting + +[source,bash] +---- +make format +---- + +=== Linting + +[source,bash] +---- +make lint +---- + +== Tree-sitter Queries + +The extractor uses sophisticated tree-sitter queries to identify metric constructors. Here's an example query for `sm::make_gauge`: + +[source,scheme] +---- +(call_expression + function: (qualified_identifier + scope: (namespace_identifier) @namespace + name: (identifier) @function_name + (#match? @namespace "sm") + (#match? @function_name "make_gauge") + ) + arguments: (argument_list + (string_literal) @metric_name + . * + (call_expression + function: (qualified_identifier + scope: (namespace_identifier) @desc_namespace + name: (identifier) @desc_function + (#match? @desc_namespace "sm") + (#match? @desc_function "description") + ) + arguments: (argument_list + (string_literal) @description + ) + )? + ) +) +---- + +== Troubleshooting + +=== Tree-sitter Compilation Issues + +If tree-sitter fails to compile: + +1. Ensure you have build tools installed: + * Linux: `sudo apt install build-essential` + * macOS: `xcode-select --install` + * Windows: Install Visual Studio Build Tools + +2. Check Python version (3.8+ required) + +3. Clear tree-sitter cache: ++ +[source,bash] +---- +make clean +---- + +=== Missing Metrics + +If expected metrics are not found: + +1. Check if the constructor patterns match the query +2. Verify file paths and namespaces +3. Enable verbose logging to see parsing details +4. Check for C++ syntax errors in source files + +=== Performance Issues + +For large codebases: + +1. Use `--filter-namespace` to limit scope +2. Process specific directories instead of entire codebase +3. Increase system memory if available + +== Contributing + +When adding support for new metric constructors: + +1. Add the tree-sitter query to `METRICS_QUERIES` in `metrics_parser.py` +2. Update `FUNCTION_TO_TYPE` mapping +3. Add tests for the new constructor pattern +4. Update documentation + +== Examples + +=== Extract Specific Namespace + +[source,bash] +---- +python metrics_extractor.py /path/to/redpanda/src \ + --filter-namespace redpanda \ + --recursive \ + --output redpanda_metrics.json +---- + +=== Compare with Previous Version + +[source,bash] +---- +# Extract current metrics +make build TAG=dev + +# Extract previous version +make build TAG=v23.3.1 + +# Compare +python compare_metrics.py autogenerated/dev/metrics/metrics.json \ + --existing autogenerated/v23.3.1/metrics/metrics.json \ + --output comparison_report.json +---- + +=== Generate Documentation Only + +[source,bash] +---- +python metrics_extractor.py /path/to/redpanda/src \ + --asciidoc redpanda_metrics.adoc \ + --filter-namespace redpanda +---- diff --git a/tools/metrics-extractor/compare_metrics.py b/tools/metrics-extractor/compare_metrics.py new file mode 100644 index 0000000..8204c5d --- /dev/null +++ b/tools/metrics-extractor/compare_metrics.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +Compare extracted metrics with existing metrics documentation +""" +import json +import sys +import argparse +import logging +from pathlib import Path + +logger = logging.getLogger("compare_metrics") + + +def load_extracted_metrics(metrics_file): + """Load metrics from the extracted JSON file""" + try: + with open(metrics_file, 'r') as f: + data = json.load(f) + return data.get('metrics', {}) + except Exception as e: + logger.error(f"Failed to load metrics file {metrics_file}: {e}") + return {} + + +def load_existing_metrics(existing_file): + """Load existing metrics from JSON file if available""" + if not Path(existing_file).exists(): + logger.info(f"No existing metrics file found at {existing_file}") + return {} + + try: + with open(existing_file, 'r') as f: + data = json.load(f) + return data.get('metrics', {}) + except Exception as e: + logger.warning(f"Failed to load existing metrics file {existing_file}: {e}") + return {} + + +def compare_metrics(extracted_metrics, existing_metrics): + """Compare extracted metrics with existing ones""" + extracted_names = set(extracted_metrics.keys()) + existing_names = set(existing_metrics.keys()) + + # Find differences + new_metrics = extracted_names - existing_names + removed_metrics = existing_names - extracted_names + common_metrics = extracted_names & existing_names + + print("=== Metrics Comparison Report ===\n") + + print(f"Total extracted metrics: {len(extracted_names)}") + print(f"Total existing metrics: {len(existing_names)}") + print(f"Common metrics: {len(common_metrics)}") + print(f"New metrics: {len(new_metrics)}") + print(f"Removed metrics: {len(removed_metrics)}\n") + + if new_metrics: + print("πŸ†• NEW METRICS:") + for metric in sorted(new_metrics): + metric_info = extracted_metrics[metric] + print(f" β€’ {metric} ({metric_info.get('type', 'unknown')})") + if metric_info.get('description'): + print(f" Description: {metric_info['description']}") + print() + + if removed_metrics: + print("❌ REMOVED METRICS:") + for metric in sorted(removed_metrics): + print(f" β€’ {metric}") + + if common_metrics: + print("πŸ“Š METRIC TYPE DISTRIBUTION (extracted):") + type_counts = {} + for metric_name in extracted_names: + metric_type = extracted_metrics[metric_name].get('type', 'unknown') + type_counts[metric_type] = type_counts.get(metric_type, 0) + 1 + + for metric_type, count in sorted(type_counts.items()): + print(f" β€’ {metric_type}: {count}") + + print("\nπŸ“ CONSTRUCTOR DISTRIBUTION:") + constructor_counts = {} + for metric_name in extracted_names: + constructor = extracted_metrics[metric_name].get('constructor', 'unknown') + constructor_counts[constructor] = constructor_counts.get(constructor, 0) + 1 + + for constructor, count in sorted(constructor_counts.items()): + print(f" β€’ {constructor}: {count}") + + return { + 'new_metrics': list(new_metrics), + 'removed_metrics': list(removed_metrics), + 'common_metrics': list(common_metrics), + 'total_extracted': len(extracted_names), + 'total_existing': len(existing_names) + } + + +def analyze_metrics_coverage(extracted_metrics): + """Analyze the coverage and quality of extracted metrics""" + print("\n=== Metrics Analysis ===\n") + + total_metrics = len(extracted_metrics) + with_description = sum(1 for m in extracted_metrics.values() if m.get('description')) + with_labels = sum(1 for m in extracted_metrics.values() if m.get('labels')) + + print(f"πŸ“ˆ COVERAGE ANALYSIS:") + print(f" β€’ Total metrics: {total_metrics}") + print(f" β€’ With descriptions: {with_description} ({with_description/total_metrics*100:.1f}%)") + print(f" β€’ With labels: {with_labels} ({with_labels/total_metrics*100:.1f}%)") + + # Analyze by namespace + namespaces = {} + for name, metric in extracted_metrics.items(): + if '_' in name: + namespace = name.split('_')[0] + namespaces[namespace] = namespaces.get(namespace, 0) + 1 + + print(f"\n🏷️ NAMESPACE DISTRIBUTION:") + for namespace, count in sorted(namespaces.items(), key=lambda x: x[1], reverse=True): + print(f" β€’ {namespace}: {count}") + + # Find metrics without descriptions + missing_descriptions = [ + name for name, metric in extracted_metrics.items() + if not metric.get('description') + ] + + if missing_descriptions: + print(f"\n⚠️ METRICS WITHOUT DESCRIPTIONS ({len(missing_descriptions)}):") + for metric in sorted(missing_descriptions)[:10]: # Show first 10 + print(f" β€’ {metric}") + if len(missing_descriptions) > 10: + print(f" ... and {len(missing_descriptions) - 10} more") + + +def main(): + parser = argparse.ArgumentParser(description="Compare extracted metrics") + parser.add_argument("metrics_file", help="JSON file with extracted metrics") + parser.add_argument("--existing", help="Existing metrics JSON file for comparison") + parser.add_argument("--output", help="Output comparison report to JSON file") + parser.add_argument("--verbose", "-v", action="store_true", help="Verbose logging") + + args = parser.parse_args() + + if args.verbose: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + # Load extracted metrics + extracted_metrics = load_extracted_metrics(args.metrics_file) + if not extracted_metrics: + logger.error("No metrics found in the extracted file") + sys.exit(1) + + # Load existing metrics if provided + existing_metrics = {} + if args.existing: + existing_metrics = load_existing_metrics(args.existing) + + # Perform comparison + if existing_metrics: + comparison_result = compare_metrics(extracted_metrics, existing_metrics) + else: + comparison_result = { + 'new_metrics': list(extracted_metrics.keys()), + 'removed_metrics': [], + 'common_metrics': [], + 'total_extracted': len(extracted_metrics), + 'total_existing': 0 + } + + # Analyze metrics + analyze_metrics_coverage(extracted_metrics) + + # Save comparison result if requested + if args.output: + with open(args.output, 'w') as f: + json.dump(comparison_result, f, indent=2) + logger.info(f"Comparison report saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/tools/metrics-extractor/debug_queries.py b/tools/metrics-extractor/debug_queries.py new file mode 100644 index 0000000..c93418e --- /dev/null +++ b/tools/metrics-extractor/debug_queries.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +""" +Debug tree-sitter queries to understand why we're not finding metrics +""" +import os +import sys +from pathlib import Path +from tree_sitter import Language, Parser + +# Add the current directory to the path so we can import our modules +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from metrics_parser import METRICS_QUERIES + +# Sample C++ code based on the user's example +SAMPLE_CODE = ''' +#include + +namespace storage { + +class something { +private: + void setup_metrics() { + _metrics.add_group("storage", { + ss::metrics::make_current_bytes( + "cached_bytes", + [this] { return _probe.cached_bytes; }, + ss::metrics::description("Size of the database in memory")), + + sm::make_gauge( + "active_connections", + [this] { return _connections.size(); }, + sm::description("Number of active connections")), + + sm::make_counter( + "requests_total", + [this] { return _total_requests; }, + sm::description("Total number of requests")), + + // More complex cases from user examples + sm::make_counter( + "errors_total", + [this] { return _audit_error_count; }, + sm::description("Running count of errors in creating/publishing " + "audit event log entries")) + .aggregate(aggregate_labels), + + {sm::make_gauge( + "buffer_usage_ratio", + [fn = std::move(get_usage_ratio)] { return fn(); }, + sm::description("Audit client send buffer usage ratio"))}, + + // Test case with different syntax + sm::make_histogram( + "request_duration_ms", + sm::description("Request duration in milliseconds")), + + ss::metrics::make_total_operations( + "disk_operations_total", + [this] { return _disk_ops; }, + ss::metrics::description("Total disk operations performed")) + }); + } + + ss::metrics::metric_groups _metrics; +}; + +} // namespace storage +''' + +def debug_tree_sitter(): + """Debug tree-sitter parsing""" + print("πŸ” Debugging tree-sitter parsing...") + + # Initialize tree-sitter + treesitter_dir = os.path.join(os.getcwd(), "tree-sitter/tree-sitter-cpp") + destination_path = os.path.join(treesitter_dir, "tree-sitter-cpp.so") + + if not os.path.exists(destination_path): + print(f"❌ Tree-sitter library not found at {destination_path}") + print("Run 'make treesitter' first") + return False + + try: + cpp_language = Language(destination_path, "cpp") + parser = Parser() + parser.set_language(cpp_language) + + print("βœ… Tree-sitter initialized successfully") + except Exception as e: + print(f"❌ Failed to initialize tree-sitter: {e}") + return False + + # Parse the sample code + tree = parser.parse(SAMPLE_CODE.encode('utf-8')) + + print(f"\nπŸ“Š Parse tree root: {tree.root_node}") + print(f"πŸ“Š Parse tree text: {tree.root_node.text[:100].decode('utf-8')}...") + + # Test each query + for query_name, query_string in METRICS_QUERIES.items(): + print(f"\nπŸ” Testing query: {query_name}") + print(f"Query: {query_string[:100]}...") + + try: + query = cpp_language.query(query_string) + captures = query.captures(tree.root_node) + + print(f"πŸ“Š Found {len(captures)} captures") + + if captures: + for node, label in captures[:5]: # Show first 5 matches + text = node.text.decode('utf-8', errors='ignore') + print(f" β€’ {label}: {text[:50]}") + + # If this is a metric name, let's see what we got + if label == "metric_name": + print(f" 🎯 FOUND METRIC: {text}") + else: + print(" ⚠️ No captures found") + + except Exception as e: + print(f" ❌ Query failed: {e}") + + # Let's also try a simple query to see if we can find any function calls + print(f"\nπŸ” Testing simple function call query...") + simple_query = """ + (call_expression + function: (qualified_identifier) @function + arguments: (argument_list) @args + ) + """ + + try: + query = cpp_language.query(simple_query) + captures = query.captures(tree.root_node) + print(f"πŸ“Š Found {len(captures)} function calls") + + metrics_found = [] + + for node, label in captures: + if label == "function": + function_text = node.text.decode('utf-8', errors='ignore') + + # Check if this is a metrics function + if any(func in function_text for func in [ + "make_gauge", "make_counter", "make_histogram", + "make_total_bytes", "make_derive", "make_current_bytes", "make_total_operations" + ]): + print(f" β€’ 🎯 METRICS FUNCTION: {function_text}") + + # Get the parent call expression to extract arguments + call_expr = node.parent + if call_expr and call_expr.type == "call_expression": + # Find the argument list + for child in call_expr.children: + if child.type == "argument_list": + args_text = child.text.decode('utf-8', errors='ignore') + print(f" πŸ“ Arguments: {args_text[:150]}...") + + # Extract metric name and description + metric_name = "" + description = "" + string_literals = [] + + # Collect all string literals + def collect_strings(node): + if node.type == "string_literal": + text = node.text.decode('utf-8', errors='ignore') + string_literals.append(text.strip('"')) + for child in node.children: + collect_strings(child) + + collect_strings(child) + + if string_literals: + metric_name = string_literals[0] + print(f" 🏷️ METRIC NAME: '{metric_name}'") + + # Look for description + if len(string_literals) > 1: + for desc in string_literals[1:]: + if len(desc) > 10: # Likely a description + description = desc + print(f" πŸ“„ DESCRIPTION: '{description[:80]}...'") + break + + # Determine metric type + metric_type = "unknown" + if "make_gauge" in function_text or "make_current_bytes" in function_text: + metric_type = "gauge" + elif "make_counter" in function_text or "make_total" in function_text or "make_derive" in function_text: + metric_type = "counter" + elif "make_histogram" in function_text: + metric_type = "histogram" + + print(f" πŸ“Š TYPE: {metric_type}") + + if metric_name: + metrics_found.append((function_text, metric_name, metric_type, description)) + break + else: + print(f" β€’ {function_text}") + + print(f"\nπŸŽ‰ SUMMARY: Found {len(metrics_found)} metrics:") + for func, name, mtype, desc in metrics_found: + print(f" β€’ '{name}' ({mtype}) via {func}") + if desc: + print(f" Description: {desc[:60]}...") + + except Exception as e: + print(f"❌ Simple query failed: {e}") + + return True + +if __name__ == "__main__": + debug_tree_sitter() diff --git a/tools/metrics-extractor/debug_test.json b/tools/metrics-extractor/debug_test.json new file mode 100644 index 0000000..00d3f12 --- /dev/null +++ b/tools/metrics-extractor/debug_test.json @@ -0,0 +1,287 @@ +{ + "metrics": { + "puts": { + "name": "puts", + "type": "counter", + "description": "Total number of files put into cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 27 + } + ], + "group_name": null, + "full_name": null + }, + "gets": { + "name": "gets", + "type": "counter", + "description": "Total number of cache get requests.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 31 + } + ], + "group_name": null, + "full_name": null + }, + "cached_gets": { + "name": "cached_gets", + "type": "counter", + "description": "Total number of get requests that are already in cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 35 + } + ], + "group_name": null, + "full_name": null + }, + "size_bytes": { + "name": "size_bytes", + "type": "gauge", + "description": "Current cache size in bytes.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 40 + } + ], + "group_name": null, + "full_name": null + }, + "files": { + "name": "files", + "type": "gauge", + "description": "Current number of files in cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 44 + } + ], + "group_name": null, + "full_name": null + }, + "in_progress_files": { + "name": "in_progress_files", + "type": "gauge", + "description": "Current number of files that are being put to cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 48 + } + ], + "group_name": null, + "full_name": null + }, + "hwm_size_bytes": { + "name": "hwm_size_bytes", + "type": "gauge", + "description": "High watermark of sum of size of cached objects.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 69 + } + ], + "group_name": null, + "full_name": null + }, + "hwm_files": { + "name": "hwm_files", + "type": "gauge", + "description": "High watermark of number of objects in cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 80 + } + ], + "group_name": null, + "full_name": null + }, + "tracker_syncs": { + "name": "tracker_syncs", + "type": "counter", + "description": "Number of times the access tracker was updated ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 86 + } + ], + "group_name": null, + "full_name": null + }, + "tracker_size": { + "name": "tracker_size", + "type": "gauge", + "description": "Number of entries in cache access tracker", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 93 + } + ], + "group_name": null, + "full_name": null + }, + "fast_trims": { + "name": "fast_trims", + "type": "counter", + "description": "Number of times we have trimmed the cache ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 103 + } + ], + "group_name": null, + "full_name": null + }, + "exhaustive_trims": { + "name": "exhaustive_trims", + "type": "counter", + "description": "Number of times we couldn't free enough space with a fast ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 109 + } + ], + "group_name": null, + "full_name": null + }, + "carryover_trims": { + "name": "carryover_trims", + "type": "counter", + "description": "Number of times we invoked carryover trim.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 116 + } + ], + "group_name": null, + "full_name": null + }, + "failed_trims": { + "name": "failed_trims", + "type": "counter", + "description": "Number of times could not free the expected amount of ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 121 + } + ], + "group_name": null, + "full_name": null + }, + "in_mem_trims": { + "name": "in_mem_trims", + "type": "counter", + "description": "Number of times we trimmed the cache using ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 128 + } + ], + "group_name": null, + "full_name": null + }, + "put": { + "name": "put", + "type": "counter", + "description": "Number of objects written into cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 141 + } + ], + "group_name": null, + "full_name": null + }, + "hit": { + "name": "hit", + "type": "counter", + "description": "Number of get requests for objects that are ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 146 + } + ], + "group_name": null, + "full_name": null + }, + "miss": { + "name": "miss", + "type": "counter", + "description": "Number of failed get requests because of ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 152 + } + ], + "group_name": null, + "full_name": null + } + }, + "statistics": { + "total_metrics": 18, + "by_type": { + "counter": 12, + "gauge": 6 + }, + "by_constructor": { + "make_counter": 12, + "make_gauge": 6 + }, + "with_description": 18, + "with_labels": 0 + } +} \ No newline at end of file diff --git a/tools/metrics-extractor/debug_test2.json b/tools/metrics-extractor/debug_test2.json new file mode 100644 index 0000000..00d3f12 --- /dev/null +++ b/tools/metrics-extractor/debug_test2.json @@ -0,0 +1,287 @@ +{ + "metrics": { + "puts": { + "name": "puts", + "type": "counter", + "description": "Total number of files put into cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 27 + } + ], + "group_name": null, + "full_name": null + }, + "gets": { + "name": "gets", + "type": "counter", + "description": "Total number of cache get requests.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 31 + } + ], + "group_name": null, + "full_name": null + }, + "cached_gets": { + "name": "cached_gets", + "type": "counter", + "description": "Total number of get requests that are already in cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 35 + } + ], + "group_name": null, + "full_name": null + }, + "size_bytes": { + "name": "size_bytes", + "type": "gauge", + "description": "Current cache size in bytes.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 40 + } + ], + "group_name": null, + "full_name": null + }, + "files": { + "name": "files", + "type": "gauge", + "description": "Current number of files in cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 44 + } + ], + "group_name": null, + "full_name": null + }, + "in_progress_files": { + "name": "in_progress_files", + "type": "gauge", + "description": "Current number of files that are being put to cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 48 + } + ], + "group_name": null, + "full_name": null + }, + "hwm_size_bytes": { + "name": "hwm_size_bytes", + "type": "gauge", + "description": "High watermark of sum of size of cached objects.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 69 + } + ], + "group_name": null, + "full_name": null + }, + "hwm_files": { + "name": "hwm_files", + "type": "gauge", + "description": "High watermark of number of objects in cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 80 + } + ], + "group_name": null, + "full_name": null + }, + "tracker_syncs": { + "name": "tracker_syncs", + "type": "counter", + "description": "Number of times the access tracker was updated ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 86 + } + ], + "group_name": null, + "full_name": null + }, + "tracker_size": { + "name": "tracker_size", + "type": "gauge", + "description": "Number of entries in cache access tracker", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 93 + } + ], + "group_name": null, + "full_name": null + }, + "fast_trims": { + "name": "fast_trims", + "type": "counter", + "description": "Number of times we have trimmed the cache ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 103 + } + ], + "group_name": null, + "full_name": null + }, + "exhaustive_trims": { + "name": "exhaustive_trims", + "type": "counter", + "description": "Number of times we couldn't free enough space with a fast ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 109 + } + ], + "group_name": null, + "full_name": null + }, + "carryover_trims": { + "name": "carryover_trims", + "type": "counter", + "description": "Number of times we invoked carryover trim.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 116 + } + ], + "group_name": null, + "full_name": null + }, + "failed_trims": { + "name": "failed_trims", + "type": "counter", + "description": "Number of times could not free the expected amount of ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 121 + } + ], + "group_name": null, + "full_name": null + }, + "in_mem_trims": { + "name": "in_mem_trims", + "type": "counter", + "description": "Number of times we trimmed the cache using ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 128 + } + ], + "group_name": null, + "full_name": null + }, + "put": { + "name": "put", + "type": "counter", + "description": "Number of objects written into cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 141 + } + ], + "group_name": null, + "full_name": null + }, + "hit": { + "name": "hit", + "type": "counter", + "description": "Number of get requests for objects that are ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 146 + } + ], + "group_name": null, + "full_name": null + }, + "miss": { + "name": "miss", + "type": "counter", + "description": "Number of failed get requests because of ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 152 + } + ], + "group_name": null, + "full_name": null + } + }, + "statistics": { + "total_metrics": 18, + "by_type": { + "counter": 12, + "gauge": 6 + }, + "by_constructor": { + "make_counter": 12, + "make_gauge": 6 + }, + "with_description": 18, + "with_labels": 0 + } +} \ No newline at end of file diff --git a/tools/metrics-extractor/enhanced_test.json b/tools/metrics-extractor/enhanced_test.json new file mode 100644 index 0000000..a61a895 --- /dev/null +++ b/tools/metrics-extractor/enhanced_test.json @@ -0,0 +1,251 @@ +{ + "metrics": { + "puts": { + "name": "puts", + "type": "counter", + "description": "Total number of files put into cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 27 + } + ] + }, + "gets": { + "name": "gets", + "type": "counter", + "description": "Total number of cache get requests.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 31 + } + ] + }, + "cached_gets": { + "name": "cached_gets", + "type": "counter", + "description": "Total number of get requests that are already in cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 35 + } + ] + }, + "size_bytes": { + "name": "size_bytes", + "type": "gauge", + "description": "Current cache size in bytes.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 40 + } + ] + }, + "files": { + "name": "files", + "type": "gauge", + "description": "Current number of files in cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 44 + } + ] + }, + "in_progress_files": { + "name": "in_progress_files", + "type": "gauge", + "description": "Current number of files that are being put to cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 48 + } + ] + }, + "hwm_size_bytes": { + "name": "hwm_size_bytes", + "type": "gauge", + "description": "High watermark of sum of size of cached objects.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 69 + } + ] + }, + "hwm_files": { + "name": "hwm_files", + "type": "gauge", + "description": "High watermark of number of objects in cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 80 + } + ] + }, + "tracker_syncs": { + "name": "tracker_syncs", + "type": "counter", + "description": "Number of times the access tracker was updated ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 86 + } + ] + }, + "tracker_size": { + "name": "tracker_size", + "type": "gauge", + "description": "Number of entries in cache access tracker", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 93 + } + ] + }, + "fast_trims": { + "name": "fast_trims", + "type": "counter", + "description": "Number of times we have trimmed the cache ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 103 + } + ] + }, + "exhaustive_trims": { + "name": "exhaustive_trims", + "type": "counter", + "description": "Number of times we couldn't free enough space with a fast ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 109 + } + ] + }, + "carryover_trims": { + "name": "carryover_trims", + "type": "counter", + "description": "Number of times we invoked carryover trim.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 116 + } + ] + }, + "failed_trims": { + "name": "failed_trims", + "type": "counter", + "description": "Number of times could not free the expected amount of ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 121 + } + ] + }, + "in_mem_trims": { + "name": "in_mem_trims", + "type": "counter", + "description": "Number of times we trimmed the cache using ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 128 + } + ] + }, + "put": { + "name": "put", + "type": "counter", + "description": "Number of objects written into cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 141 + } + ] + }, + "hit": { + "name": "hit", + "type": "counter", + "description": "Number of get requests for objects that are ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 146 + } + ] + }, + "miss": { + "name": "miss", + "type": "counter", + "description": "Number of failed get requests because of ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 152 + } + ] + } + }, + "statistics": { + "total_metrics": 18, + "by_type": { + "counter": 12, + "gauge": 6 + }, + "by_constructor": { + "make_counter": 12, + "make_gauge": 6 + }, + "with_description": 18, + "with_labels": 0 + } +} \ No newline at end of file diff --git a/tools/metrics-extractor/example.py b/tools/metrics-extractor/example.py new file mode 100644 index 0000000..b990f7b --- /dev/null +++ b/tools/metrics-extractor/example.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +""" +Example script demonstrating the metrics extractor usage +""" +import argparse +import tempfile +import os +from pathlib import Path + +# Sample C++ code with metrics that would be found in Redpanda +REDPANDA_SAMPLE = ''' +// File: src/v/kafka/server/handlers/produce.cc +#include + +namespace kafka { + +class produce_handler { +private: + void setup_metrics() { + _metrics.add_group("kafka", { + sm::make_counter( + "produce_requests_total", + [this] { return _produce_requests; }, + sm::description("Total number of produce requests received")), + + sm::make_gauge( + "active_connections", + [this] { return _connections.size(); }, + sm::description("Number of currently active Kafka connections")), + + sm::make_histogram( + "produce_latency_seconds", + sm::description("Latency histogram for Kafka produce requests")), + + sm::make_total_bytes( + "bytes_produced_total", + [this] { return _bytes_produced; }, + sm::description("Total bytes produced to Kafka topics")) + }); + } + + uint64_t _produce_requests = 0; + uint64_t _bytes_produced = 0; + std::vector _connections; + ss::metrics::metric_groups _metrics; +}; + +} // namespace kafka +''' + +CLUSTER_SAMPLE = ''' +// File: src/v/cluster/partition_manager.cc +#include + +namespace cluster { + +class partition_manager { +private: + void register_metrics() { + _metrics.add_group("redpanda_cluster", { + sm::make_gauge( + "partitions", + [this] { return _partitions.size(); }, + sm::description("Number of partitions in the cluster")), + + sm::make_counter( + "leadership_changes", + [this] { return _leadership_changes; }, + sm::description("Number of leadership changes across all partitions")), + + ss::metrics::make_current_bytes( + "memory_usage_bytes", + [this] { return _memory_tracker.used(); }, + ss::metrics::description("Current memory usage by partition manager")) + }); + } + + std::unordered_map _partitions; + uint64_t _leadership_changes = 0; + memory_tracker _memory_tracker; + ss::metrics::metric_groups _metrics; +}; + +} // namespace cluster +''' + + +def create_sample_files(temp_dir): + """Create sample C++ files for testing""" + files = [] + + # Create kafka handler file + kafka_file = temp_dir / "kafka_produce.cc" + with open(kafka_file, 'w') as f: + f.write(REDPANDA_SAMPLE) + files.append(kafka_file) + + # Create cluster manager file + cluster_file = temp_dir / "cluster_partition_manager.cc" + with open(cluster_file, 'w') as f: + f.write(CLUSTER_SAMPLE) + files.append(cluster_file) + + return files + + +def run_example(): + """Run the metrics extraction example""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + print("πŸ”§ Creating sample C++ files...") + sample_files = create_sample_files(temp_path) + + print("πŸ“ Sample files created:") + for f in sample_files: + print(f" β€’ {f.name}") + + try: + from metrics_parser import get_treesitter_cpp_parser_and_language, parse_cpp_file + from metrics_bag import MetricsBag + + print("\n🌳 Initializing tree-sitter C++ parser...") + parser, language = get_treesitter_cpp_parser_and_language("tree-sitter", "tree-sitter-cpp.so") + + print("πŸ” Extracting metrics from sample files...") + all_metrics = MetricsBag() + + for cpp_file in sample_files: + print(f" Processing {cpp_file.name}...") + file_metrics = parse_cpp_file(cpp_file, parser, language) + all_metrics.merge(file_metrics) + + # Display results + print(f"\nβœ… Extraction completed! Found {len(all_metrics)} metrics:") + print() + + for name, metric in all_metrics.get_all_metrics().items(): + print(f"πŸ“Š {name}") + print(f" Type: {metric['type']}") + print(f" Description: {metric.get('description', 'No description')}") + if metric.get('labels'): + print(f" Labels: {', '.join(metric['labels'])}") + print(f" Constructor: {metric['constructor']}") + print(f" File: {metric['files'][0]['file']}") + print() + + # Show statistics + stats = all_metrics.get_statistics() + print("πŸ“ˆ Statistics:") + print(f" Total metrics: {stats['total_metrics']}") + print(f" By type: {stats['by_type']}") + print(f" By constructor: {stats['by_constructor']}") + print(f" With descriptions: {stats['with_description']}") + print() + + return True + + except ImportError as e: + print(f"❌ Import error: {e}") + print("Make sure to install dependencies with: pip install -r requirements.txt") + return False + except Exception as e: + print(f"❌ Error during extraction: {e}") + return False + + +def main(): + parser = argparse.ArgumentParser(description="Redpanda metrics extractor example") + parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output") + + args = parser.parse_args() + + if args.verbose: + import logging + logging.basicConfig(level=logging.DEBUG) + + print("πŸš€ Redpanda Metrics Extractor Example") + print("=====================================") + print() + print("This example demonstrates how the metrics extractor works") + print("by processing sample C++ code with Redpanda metrics.") + print() + + success = run_example() + + if success: + print("πŸŽ‰ Example completed successfully!") + print() + print("Next steps:") + print(" 1. Run against real Redpanda source: make build TAG=dev") + print(" 2. Compare with existing metrics: python compare_metrics.py metrics.json") + print(" 3. Generate documentation: see README.adoc for details") + else: + print("❌ Example failed. Please check the error messages above.") + return 1 + + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/tools/metrics-extractor/metrics.json b/tools/metrics-extractor/metrics.json new file mode 100644 index 0000000..f2720a2 --- /dev/null +++ b/tools/metrics-extractor/metrics.json @@ -0,0 +1,5036 @@ +{ + "metrics": { + "puts": { + "name": "puts", + "type": "counter", + "description": "Total number of files put into cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 27 + } + ] + }, + "gets": { + "name": "gets", + "type": "counter", + "description": "Total number of cache get requests.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 31 + } + ] + }, + "cached_gets": { + "name": "cached_gets", + "type": "counter", + "description": "Total number of get requests that are already in cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 35 + } + ] + }, + "size_bytes": { + "name": "size_bytes", + "type": "gauge", + "description": "Current cache size in bytes.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 40 + } + ] + }, + "files": { + "name": "files", + "type": "gauge", + "description": "Current number of files in cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 44 + } + ] + }, + "in_progress_files": { + "name": "in_progress_files", + "type": "gauge", + "description": "Current number of files that are being put to cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 48 + } + ] + }, + "hwm_size_bytes": { + "name": "hwm_size_bytes", + "type": "gauge", + "description": "High watermark of sum of size of cached objects.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 69 + } + ] + }, + "hwm_files": { + "name": "hwm_files", + "type": "gauge", + "description": "High watermark of number of objects in cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 80 + } + ] + }, + "tracker_syncs": { + "name": "tracker_syncs", + "type": "counter", + "description": "Number of times the access tracker was updated ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 86 + } + ] + }, + "tracker_size": { + "name": "tracker_size", + "type": "gauge", + "description": "Number of entries in cache access tracker", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 93 + } + ] + }, + "fast_trims": { + "name": "fast_trims", + "type": "counter", + "description": "Number of times we have trimmed the cache ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 103 + } + ] + }, + "exhaustive_trims": { + "name": "exhaustive_trims", + "type": "counter", + "description": "Number of times we couldn't free enough space with a fast ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 109 + } + ] + }, + "carryover_trims": { + "name": "carryover_trims", + "type": "counter", + "description": "Number of times we invoked carryover trim.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 116 + } + ] + }, + "failed_trims": { + "name": "failed_trims", + "type": "counter", + "description": "Number of times could not free the expected amount of ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 121 + } + ] + }, + "in_mem_trims": { + "name": "in_mem_trims", + "type": "counter", + "description": "Number of times we trimmed the cache using ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 128 + } + ] + }, + "put": { + "name": "put", + "type": "counter", + "description": "Number of objects written into cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 141 + } + ] + }, + "hit": { + "name": "hit", + "type": "counter", + "description": "Number of get requests for objects that are ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 146 + } + ] + }, + "miss": { + "name": "miss", + "type": "counter", + "description": "Number of failed get requests because of ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 152 + } + ] + }, + "read_bytes": { + "name": "read_bytes", + "type": "counter", + "description": "Total bytes read from remote partition", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 36 + }, + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 123 + }, + { + "file": "tmp/redpanda-dev/src/v/transform/probe.cc", + "line": 31 + } + ] + }, + "read_records": { + "name": "read_records", + "type": "counter", + "description": "Total number of records read from remote partition", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 42 + } + ] + }, + "chunk_size": { + "name": "chunk_size", + "type": "gauge", + "description": "Size of chunk downloaded from cloud storage", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 48 + } + ] + }, + "downloads_throttled_sum": { + "name": "downloads_throttled_sum", + "type": "counter", + "description": "Total amount of time downloads were throttled (ms)", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 66 + } + ] + }, + "materialized_segments": { + "name": "materialized_segments", + "type": "gauge", + "description": "Current number of materialized remote segments", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 79 + } + ] + }, + "readers": { + "name": "readers", + "type": "gauge", + "description": "Current number of remote partition readers", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 85 + }, + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 206 + } + ] + }, + "spillover_manifest_bytes": { + "name": "spillover_manifest_bytes", + "type": "gauge", + "description": "Total amount of memory used by spillover manifests", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 91 + } + ] + }, + "spillover_manifest_instances": { + "name": "spillover_manifest_instances", + "type": "gauge", + "description": "Total number of spillover manifests stored in memory", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 98 + } + ] + }, + "spillover_manifest_hydrated": { + "name": "spillover_manifest_hydrated", + "type": "counter", + "description": "Number of times spillover manifests were saved to the cache", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 105 + } + ] + }, + "spillover_manifest_materialized": { + "name": "spillover_manifest_materialized", + "type": "counter", + "description": "Number of times spillover manifests were loaded ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 112 + } + ] + }, + "segment_readers": { + "name": "segment_readers", + "type": "gauge", + "description": "Current number of remote segment readers", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 119 + } + ] + }, + "spillover_manifest_latency": { + "name": "spillover_manifest_latency", + "type": "histogram", + "description": "Spillover manifest materialization latency histogram", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 125 + } + ] + }, + "chunks_hydrated": { + "name": "chunks_hydrated", + "type": "counter", + "description": "Total number of hydrated chunks (some may have been ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 134 + } + ] + }, + "chunk_hydration_latency": { + "name": "chunk_hydration_latency", + "type": "histogram", + "description": "Chunk hydration latency histogram", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 142 + } + ] + }, + "hydrations_in_progress": { + "name": "hydrations_in_progress", + "type": "counter", + "description": "Active hydrations in progress", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", + "line": 149 + } + ] + }, + "topic_manifest_uploads": { + "name": "topic_manifest_uploads", + "type": "counter", + "description": "Number of topic manifest uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 34 + } + ] + }, + "partition_manifest_uploads": { + "name": "partition_manifest_uploads", + "type": "counter", + "description": "Number of partition manifest (re)uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 38 + } + ] + }, + "topic_manifest_downloads": { + "name": "topic_manifest_downloads", + "type": "counter", + "description": "Number of topic manifest downloads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 42 + } + ] + }, + "partition_manifest_downloads": { + "name": "partition_manifest_downloads", + "type": "counter", + "description": "Number of partition manifest downloads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 46 + } + ] + }, + "cluster_metadata_manifest_uploads": { + "name": "cluster_metadata_manifest_uploads", + "type": "counter", + "description": "Number of partition manifest uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 50 + } + ] + }, + "cluster_metadata_manifest_downloads": { + "name": "cluster_metadata_manifest_downloads", + "type": "counter", + "description": "Number of partition manifest downloads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 54 + } + ] + }, + "manifest_upload_backoff": { + "name": "manifest_upload_backoff", + "type": "counter", + "description": "Number of times backoff was applied during manifest upload", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 58 + } + ] + }, + "manifest_download_backoff": { + "name": "manifest_download_backoff", + "type": "counter", + "description": "Number of times backoff was applied during ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 63 + } + ] + }, + "successful_uploads": { + "name": "successful_uploads", + "type": "counter", + "description": "Number of completed log-segment uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 68 + } + ] + }, + "successful_downloads": { + "name": "successful_downloads", + "type": "counter", + "description": "Number of completed log-segment downloads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 72 + } + ] + }, + "failed_uploads": { + "name": "failed_uploads", + "type": "counter", + "description": "Number of failed log-segment uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 76 + } + ] + }, + "failed_downloads": { + "name": "failed_downloads", + "type": "counter", + "description": "Number of failed log-segment downloads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 80 + } + ] + }, + "failed_manifest_uploads": { + "name": "failed_manifest_uploads", + "type": "counter", + "description": "Number of failed manifest uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 84 + } + ] + }, + "failed_manifest_downloads": { + "name": "failed_manifest_downloads", + "type": "counter", + "description": "Number of failed manifest downloads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 88 + } + ] + }, + "upload_backoff": { + "name": "upload_backoff", + "type": "counter", + "description": "Number of times backoff was applied during ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 92 + }, + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 257 + } + ] + }, + "download_backoff": { + "name": "download_backoff", + "type": "counter", + "description": "Number of times backoff was applied during ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 97 + }, + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 251 + } + ] + }, + "bytes_sent": { + "name": "bytes_sent", + "type": "counter", + "description": "Number of bytes sent to cloud storage", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 102 + }, + { + "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", + "line": 142 + } + ] + }, + "bytes_received": { + "name": "bytes_received", + "type": "counter", + "description": "Number of bytes received from cloud storage", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 106 + }, + { + "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", + "line": 135 + } + ] + }, + "index_uploads": { + "name": "index_uploads", + "type": "counter", + "description": "Number of segment indices uploaded", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 110 + } + ] + }, + "index_downloads": { + "name": "index_downloads", + "type": "counter", + "description": "Number of segment indices downloaded", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 114 + } + ] + }, + "failed_index_uploads": { + "name": "failed_index_uploads", + "type": "counter", + "description": "Number of failed segment index uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 118 + } + ] + }, + "failed_index_downloads": { + "name": "failed_index_downloads", + "type": "counter", + "description": "Number of failed segment index downloads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 122 + } + ] + }, + "spillover_manifest_uploads": { + "name": "spillover_manifest_uploads", + "type": "counter", + "description": "Number of spillover manifest (re)uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 126 + } + ] + }, + "spillover_manifest_downloads": { + "name": "spillover_manifest_downloads", + "type": "counter", + "description": "Number of spillover manifest downloads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 130 + } + ] + }, + "controller_snapshot_successful_uploads": { + "name": "controller_snapshot_successful_uploads", + "type": "counter", + "description": "Number of completed controller snapshot uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 134 + } + ] + }, + "controller_snapshot_failed_uploads": { + "name": "controller_snapshot_failed_uploads", + "type": "counter", + "description": "Number of failed controller snapshot uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 139 + } + ] + }, + "controller_snapshot_upload_backoff": { + "name": "controller_snapshot_upload_backoff", + "type": "counter", + "description": "Number of times backoff was applied during ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 143 + } + ] + }, + "client_acquisition_latency": { + "name": "client_acquisition_latency", + "type": "histogram", + "description": "Client acquisition latency histogram", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 148 + } + ] + }, + "segment_download_latency": { + "name": "segment_download_latency", + "type": "histogram", + "description": "Segment download latency histogram", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 154 + } + ] + }, + "errors_total": { + "name": "errors_total", + "type": "counter", + "description": "Number of transmit errors", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 169 + }, + { + "file": "tmp/redpanda-dev/src/v/security/oidc_service.cc", + "line": 99 + }, + { + "file": "tmp/redpanda-dev/src/v/security/audit/probes.cc", + "line": 40 + } + ] + }, + "partition_manifest_uploads_total": { + "name": "partition_manifest_uploads_total", + "type": "counter", + "description": "Successful partition manifest uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 188 + } + ] + }, + "segment_uploads_total": { + "name": "segment_uploads_total", + "type": "counter", + "description": "Successful data segment uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 194 + } + ] + }, + "active_segments": { + "name": "active_segments", + "type": "gauge", + "description": "Number of remote log segments currently hydrated for read", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 200 + } + ] + }, + "partition_readers": { + "name": "partition_readers", + "type": "gauge", + "description": "Number of partition reader instances (number of current ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 212 + } + ] + }, + "partition_readers_delayed": { + "name": "partition_readers_delayed", + "type": "counter", + "description": "How many partition reades were delayed due to ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 219 + } + ] + }, + "segment_readers_delayed": { + "name": "segment_readers_delayed", + "type": "counter", + "description": "How many segment readers were delayed due to ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 226 + } + ] + }, + "segment_materializations_delayed": { + "name": "segment_materializations_delayed", + "type": "counter", + "description": "How many segment materializations were delayed due to ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 233 + } + ] + }, + "segment_index_uploads_total": { + "name": "segment_index_uploads_total", + "type": "counter", + "description": "Successful segment index uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 241 + } + ] + }, + "spillover_manifest_uploads_total": { + "name": "spillover_manifest_uploads_total", + "type": "counter", + "description": "Successful spillover manifest uploads", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 247 + } + ] + }, + "spillover_manifests_materialized_count": { + "name": "spillover_manifests_materialized_count", + "type": "gauge", + "description": "How many spilled manifests are currently cached in memory", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 253 + } + ] + }, + "spillover_manifests_materialized_bytes": { + "name": "spillover_manifests_materialized_bytes", + "type": "gauge", + "description": "Bytes of memory used for spilled manifests ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", + "line": 260 + } + ] + }, + "total_uploads": { + "name": "total_uploads", + "type": "counter", + "description": "Number of completed PUT requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 125 + } + ] + }, + "total_downloads": { + "name": "total_downloads", + "type": "counter", + "description": "Number of completed GET requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 130 + } + ] + }, + "all_requests": { + "name": "all_requests", + "type": "counter", + "description": "Number of completed HTTP requests (includes PUT and GET)", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 135 + } + ] + }, + "active_uploads": { + "name": "active_uploads", + "type": "gauge", + "description": "Number of active PUT requests at the moment", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 141 + } + ] + }, + "active_downloads": { + "name": "active_downloads", + "type": "gauge", + "description": "Number of active GET requests at the moment", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 146 + } + ] + }, + "active_requests": { + "name": "active_requests", + "type": "gauge", + "description": "Number of active HTTP requests at the moment ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 151 + }, + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 72 + } + ] + }, + "total_inbound_bytes": { + "name": "total_inbound_bytes", + "type": "counter", + "description": "Total number of bytes received from cloud storage", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 157 + }, + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 79 + } + ] + }, + "total_outbound_bytes": { + "name": "total_outbound_bytes", + "type": "counter", + "description": "Total number of bytes sent to cloud storage", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 162 + }, + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 86 + } + ] + }, + "num_rpc_errors": { + "name": "num_rpc_errors", + "type": "counter", + "description": "Total number of REST API errors received from ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 167 + } + ] + }, + "num_transport_errors": { + "name": "num_transport_errors", + "type": "counter", + "description": "Total number of transport errors (TCP and TLS)", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 173 + }, + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 93 + } + ] + }, + "num_slowdowns": { + "name": "num_slowdowns", + "type": "counter", + "description": "Total number of SlowDown errors received from cloud ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 178 + } + ] + }, + "num_nosuchkey": { + "name": "num_nosuchkey", + "type": "counter", + "description": "Total number of NoSuchKey errors received from cloud ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 184 + } + ] + }, + "num_borrows": { + "name": "num_borrows", + "type": "counter", + "description": "Number of time current shard had to borrow a cloud ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 191 + } + ] + }, + "lease_duration": { + "name": "lease_duration", + "type": "histogram", + "description": "Lease duration histogram", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 197 + } + ] + }, + "client_pool_utilization": { + "name": "client_pool_utilization", + "type": "gauge", + "description": "Utilization of the cloud storage pool(0 - unused, ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 202 + } + ] + }, + "lease_timeouts_total": { + "name": "lease_timeouts_total", + "type": "counter", + "description": "Number of cloud storage client lease timeouts, usually indicating ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 208 + } + ] + }, + "not_found": { + "name": "not_found", + "type": "counter", + "description": "Total number of requests for which the object was not found", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 238 + } + ] + }, + "backoff": { + "name": "backoff", + "type": "counter", + "description": "Total number of requests that backed off", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 245 + } + ] + }, + "uploads": { + "name": "uploads", + "type": "counter", + "description": "Total number of requests that uploaded an object to ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 263 + } + ] + }, + "downloads": { + "name": "downloads", + "type": "counter", + "description": "Total number of requests that downloaded an object ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", + "line": 270 + } + ] + }, + "pending_partition_operations": { + "name": "pending_partition_operations", + "type": "gauge", + "description": "Number of partitions with ongoing/requested operations", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/controller_backend.cc", + "line": 341 + } + ] + }, + "requests_dropped": { + "name": "requests_dropped", + "type": "counter", + "description": "Controller log rate limiting. ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/controller_log_limiter.cc", + "line": 69 + } + ] + }, + "requests_available_rps": { + "name": "requests_available_rps", + "type": "gauge", + "description": "Controller log rate limiting.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/controller_log_limiter.cc", + "line": 77 + } + ] + }, + "brokers": { + "name": "brokers", + "type": "gauge", + "description": "Number of configured brokers in the cluster", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", + "line": 72 + } + ] + }, + "topics": { + "name": "topics", + "type": "gauge", + "description": "Number of topics in the cluster", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", + "line": 81 + } + ] + }, + "partitions": { + "name": "partitions", + "type": "gauge", + "description": "Number of partitions in the cluster (replicas not included)", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", + "line": 89 + }, + { + "file": "tmp/redpanda-dev/src/v/cluster/topic_table_probe.cc", + "line": 163 + } + ] + }, + "unavailable_partitions": { + "name": "unavailable_partitions", + "type": "gauge", + "description": "Number of partitions that lack quorum among replicants", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", + "line": 97 + } + ] + }, + "non_homogenous_fips_mode": { + "name": "non_homogenous_fips_mode", + "type": "gauge", + "description": "Number of nodes that have a non-homogenous FIPS mode value", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", + "line": 108 + } + ] + }, + "latest_cluster_metadata_manifest_age": { + "name": "latest_cluster_metadata_manifest_age", + "type": "gauge", + "description": "Age in seconds of the latest ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", + "line": 132 + } + ] + }, + "queued_node_operations": { + "name": "queued_node_operations", + "type": "gauge", + "description": "Number of queued node operations", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/members_backend.cc", + "line": 72 + } + ] + }, + "rpcs_sent": { + "name": "rpcs_sent", + "type": "gauge", + "description": "Number of node status RPCs sent by this node", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/node_status_backend.cc", + "line": 366 + } + ] + }, + "rpcs_timed_out": { + "name": "rpcs_timed_out", + "type": "gauge", + "description": "Number of timed out node status RPCs from this node", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/node_status_backend.cc", + "line": 371 + } + ] + }, + "rpcs_received": { + "name": "rpcs_received", + "type": "gauge", + "description": "Number of node status RPCs received by this node", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/node_status_backend.cc", + "line": 377 + } + ] + }, + "num_with_broken_rack_constraint": { + "name": "num_with_broken_rack_constraint", + "type": "gauge", + "description": "Number of partitions that don't satisfy the rack ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_balancer_state.cc", + "line": 156 + } + ] + }, + "leader_id": { + "name": "leader_id", + "type": "gauge", + "description": "Id of current partition leader", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 83 + } + ] + }, + "under_replicated_replicas": { + "name": "under_replicated_replicas", + "type": "gauge", + "description": "Number of under replicated replicas", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 91 + } + ] + }, + "leader": { + "name": "leader", + "type": "gauge", + "description": "Flag indicating if this partition instance is a leader", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 104 + } + ] + }, + "start_offset": { + "name": "start_offset", + "type": "gauge", + "description": "start offset", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 110 + } + ] + }, + "last_stable_offset": { + "name": "last_stable_offset", + "type": "gauge", + "description": "Last stable offset", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 115 + } + ] + }, + "committed_offset": { + "name": "committed_offset", + "type": "gauge", + "description": "Partition commited offset. i.e. safely persisted on ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 120 + } + ] + }, + "end_offset": { + "name": "end_offset", + "type": "gauge", + "description": "Last offset stored by current partition on this node", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 126 + } + ] + }, + "high_watermark": { + "name": "high_watermark", + "type": "gauge", + "description": "Partion high watermark i.e. highest consumable offset", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 132 + } + ] + }, + "records_produced": { + "name": "records_produced", + "type": "counter", + "description": "Total number of records produced", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 138 + } + ] + }, + "batches_produced": { + "name": "batches_produced", + "type": "gauge", + "description": "Total number of batches produced", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 143 + } + ] + }, + "records_fetched": { + "name": "records_fetched", + "type": "counter", + "description": "Total number of records fetched", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 148 + } + ] + }, + "bytes_produced_total": { + "name": "bytes_produced_total", + "type": "counter", + "description": "Total number of bytes produced", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 153 + } + ] + }, + "bytes_fetched_total": { + "name": "bytes_fetched_total", + "type": "counter", + "description": "Total number of bytes fetched (not all might be ", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 158 + } + ] + }, + "bytes_fetched_from_follower_total": { + "name": "bytes_fetched_from_follower_total", + "type": "counter", + "description": "Total number of bytes fetched from follower (not all might be ", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 164 + } + ] + }, + "cloud_storage_segments_metadata_bytes": { + "name": "cloud_storage_segments_metadata_bytes", + "type": "counter", + "description": "Current number of bytes consumed by remote segments ", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 171 + } + ] + }, + "iceberg_offsets_pending_translation": { + "name": "iceberg_offsets_pending_translation", + "type": "gauge", + "description": "Total number of offsets that are pending ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 196 + } + ] + }, + "iceberg_offsets_pending_commit": { + "name": "iceberg_offsets_pending_commit", + "type": "gauge", + "description": "Total number of offsets that are pending ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 210 + } + ] + }, + "schema_id_validation_records_failed": { + "name": "schema_id_validation_records_failed", + "type": "counter", + "description": "Number of records that failed schema ID validation", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 234 + } + ] + }, + "max_offset": { + "name": "max_offset", + "type": "gauge", + "description": "Latest readable offset of the partition (i.e. high watermark)", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 272 + } + ] + }, + "request_bytes_total": { + "name": "request_bytes_total", + "type": "counter", + "description": "produce", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 314 + } + ] + }, + "records_produced_total": { + "name": "records_produced_total", + "type": "counter", + "description": "Total number of records produced", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 332 + } + ] + }, + "records_fetched_total": { + "name": "records_fetched_total", + "type": "counter", + "description": "Total number of records fetched", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 338 + } + ] + }, + "anomalies": { + "name": "anomalies", + "type": "gauge", + "description": "", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", + "line": 407 + } + ] + }, + "producer_manager_total_active_producers": { + "name": "producer_manager_total_active_producers", + "type": "gauge", + "description": "Total number of active idempotent and transactional producers.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/producer_state_manager.cc", + "line": 61 + } + ] + }, + "evicted_producers": { + "name": "evicted_producers", + "type": "counter", + "description": "Number of evicted producers so far.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/producer_state_manager.cc", + "line": 66 + } + ] + }, + "idempotency_pid_cache_size": { + "name": "idempotency_pid_cache_size", + "type": "gauge", + "description": "Number of active producers (known producer_id seq number pairs).", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/rm_stm.cc", + "line": 2212 + } + ] + }, + "tx_num_inflight_requests": { + "name": "tx_num_inflight_requests", + "type": "gauge", + "description": "Number of ongoing transactional requests.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/rm_stm.cc", + "line": 2218 + } + ] + }, + "assigned_partitions": { + "name": "assigned_partitions", + "type": "gauge", + "description": "Number of partitions assigned to this shard", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/shard_placement_table_probe.cc", + "line": 25 + } + ] + }, + "hosted_partitions": { + "name": "hosted_partitions", + "type": "gauge", + "description": "Number of partitions hosted on this shard", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/shard_placement_table_probe.cc", + "line": 29 + } + ] + }, + "partitions_to_reconcile": { + "name": "partitions_to_reconcile", + "type": "gauge", + "description": "Number of partitions needing reconciliation of ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/shard_placement_table_probe.cc", + "line": 33 + } + ] + }, + "remade_partitions": { + "name": "remade_partitions", + "type": "gauge", + "description": "Number of partitions that were forced to be remade", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/shard_placement_table_probe.cc", + "line": 38 + } + ] + }, + "moving_to_node": { + "name": "moving_to_node", + "type": "gauge", + "description": "Amount of partitions that are moving to node", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/topic_table_probe.cc", + "line": 45 + } + ] + }, + "moving_from_node": { + "name": "moving_from_node", + "type": "gauge", + "description": "Amount of partitions that are moving from node", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/topic_table_probe.cc", + "line": 50 + } + ] + }, + "node_cancelling_movements": { + "name": "node_cancelling_movements", + "type": "gauge", + "description": "Amount of cancelling partition movements for node", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/topic_table_probe.cc", + "line": 55 + } + ] + }, + "replicas": { + "name": "replicas", + "type": "gauge", + "description": "Configured number of replicas for the topic", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/topic_table_probe.cc", + "line": 150 + } + ] + }, + "uploaded": { + "name": "uploaded", + "type": "counter", + "description": "Uploaded offsets", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 58 + } + ] + }, + "uploaded_bytes": { + "name": "uploaded_bytes", + "type": "counter", + "description": "Total number of uploaded bytes", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 63 + } + ] + }, + "missing": { + "name": "missing", + "type": "counter", + "description": "Missing offsets due to gaps", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 68 + } + ] + }, + "pending": { + "name": "pending", + "type": "gauge", + "description": "Pending offsets", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 73 + } + ] + }, + "compacted_replaced_bytes": { + "name": "compacted_replaced_bytes", + "type": "gauge", + "description": "Bytes replaced due to compaction since this replica ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 78 + } + ] + }, + "deleted_segments": { + "name": "deleted_segments", + "type": "counter", + "description": "Number of segments that have been deleted from S3 for the topic. ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 115 + } + ] + }, + "segments": { + "name": "segments", + "type": "gauge", + "description": "Total number of accounted segments in the cloud for the topic", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 124 + } + ] + }, + "segments_pending_deletion": { + "name": "segments_pending_deletion", + "type": "gauge", + "description": "Total number of segments pending deletion from the ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 131 + } + ] + }, + "cloud_log_size": { + "name": "cloud_log_size", + "type": "gauge", + "description": "Total size in bytes of the user-visible log for the topic", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 154 + } + ] + }, + "paused_archivers": { + "name": "paused_archivers", + "type": "gauge", + "description": "Number of paused archivers", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 161 + } + ] + }, + "rounds": { + "name": "rounds", + "type": "counter", + "description": "Number of upload housekeeping rounds", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 179 + } + ] + }, + "jobs_completed": { + "name": "jobs_completed", + "type": "counter", + "description": "Number of executed housekeeping jobs", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 184 + } + ] + }, + "jobs_failed": { + "name": "jobs_failed", + "type": "counter", + "description": "Number of failed housekeeping jobs", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 189 + } + ] + }, + "jobs_skipped": { + "name": "jobs_skipped", + "type": "counter", + "description": "Number of skipped housekeeping jobs", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 194 + } + ] + }, + "resumes": { + "name": "resumes", + "type": "gauge", + "description": "Number of times upload housekeeping was resumed", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 199 + } + ] + }, + "pauses": { + "name": "pauses", + "type": "gauge", + "description": "Number of times upload housekeeping was paused", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 204 + } + ] + }, + "drains": { + "name": "drains", + "type": "gauge", + "description": "Number of times upload housekeeping queue was drained", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 209 + } + ] + }, + "requests_throttled_average_rate": { + "name": "requests_throttled_average_rate", + "type": "gauge", + "description": "Average rate of requests from the read and write ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 215 + } + ] + }, + "local_segment_reuploads": { + "name": "local_segment_reuploads", + "type": "gauge", + "description": "Number of segment reuploads from local data directory", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 225 + } + ] + }, + "cloud_segment_reuploads": { + "name": "cloud_segment_reuploads", + "type": "gauge", + "description": "Number of segment reuploads from cloud storage sources (cloud ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 231 + } + ] + }, + "manifest_reuploads": { + "name": "manifest_reuploads", + "type": "gauge", + "description": "Number of manifest reuploads performed by all housekeeping jobs", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 238 + } + ] + }, + "segment_deletions": { + "name": "segment_deletions", + "type": "gauge", + "description": "Number of segments deleted by all housekeeping jobs", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 244 + } + ] + }, + "metadata_syncs": { + "name": "metadata_syncs", + "type": "gauge", + "description": "Number of archival configuration updates performed ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", + "line": 250 + } + ] + }, + "leader_transfer_error": { + "name": "leader_transfer_error", + "type": "counter", + "description": "Number of errors attempting to transfer leader", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/scheduling/leader_balancer_probe.cc", + "line": 25 + } + ] + }, + "leader_transfer_succeeded": { + "name": "leader_transfer_succeeded", + "type": "counter", + "description": "Number of successful leader transfers", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/scheduling/leader_balancer_probe.cc", + "line": 29 + } + ] + }, + "leader_transfer_timeout": { + "name": "leader_transfer_timeout", + "type": "counter", + "description": "Number of timeouts attempting to transfer leader", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/scheduling/leader_balancer_probe.cc", + "line": 33 + } + ] + }, + "leader_transfer_no_improvement": { + "name": "leader_transfer_no_improvement", + "type": "counter", + "description": "Number of times no balance improvement was found", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cluster/scheduling/leader_balancer_probe.cc", + "line": 37 + } + ] + }, + "backlog_size": { + "name": "backlog_size", + "type": "gauge", + "description": "Iceberg controller current backlog - averaged size ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/backlog_controller.cc", + "line": 95 + }, + { + "file": "tmp/redpanda-dev/src/v/storage/backlog_controller.cc", + "line": 149 + } + ] + }, + "misses": { + "name": "misses", + "type": "counter", + "description": "The number of times a schema wasn't in the cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/record_schema_resolver.cc", + "line": 307 + } + ] + }, + "hits": { + "name": "hits", + "type": "counter", + "description": "The number of times a schema was in the cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/record_schema_resolver.cc", + "line": 314 + } + ] + }, + "translations_finished": { + "name": "translations_finished", + "type": "counter", + "description": "Number of finished translator executions", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", + "line": 49 + } + ] + }, + "files_created": { + "name": "files_created", + "type": "counter", + "description": "Number of created parquet files (not counting the DLQ table)", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", + "line": 58 + } + ] + }, + "parquet_rows_added": { + "name": "parquet_rows_added", + "type": "counter", + "description": "Number of rows in created parquet files (not ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", + "line": 68 + } + ] + }, + "parquet_bytes_added": { + "name": "parquet_bytes_added", + "type": "counter", + "description": "Number of bytes in created parquet files (not ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", + "line": 78 + } + ] + }, + "dlq_files_created": { + "name": "dlq_files_created", + "type": "counter", + "description": "Number of created parquet files for the DLQ table", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", + "line": 88 + } + ] + }, + "raw_bytes_processed": { + "name": "raw_bytes_processed", + "type": "counter", + "description": "Number of raw, potentially compressed bytes consumed for ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", + "line": 110 + } + ] + }, + "decompressed_bytes_processed": { + "name": "decompressed_bytes_processed", + "type": "counter", + "description": "Number of bytes post-decompression consumed for processing that ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", + "line": 125 + } + ] + }, + "invalid_records": { + "name": "invalid_records", + "type": "counter", + "description": "Number of invalid records handled by translation", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", + "line": 159 + } + ] + }, + "last_successful_bundle_timestamp_seconds": { + "name": "last_successful_bundle_timestamp_seconds", + "type": "gauge", + "description": "Timestamp of last successful debug bundle ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/debug_bundle/probe.cc", + "line": 34 + } + ] + }, + "last_failed_bundle_timestamp_seconds": { + "name": "last_failed_bundle_timestamp_seconds", + "type": "gauge", + "description": "Timestamp of last failed debug bundle ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/debug_bundle/probe.cc", + "line": 41 + } + ] + }, + "successful_generation_count": { + "name": "successful_generation_count", + "type": "counter", + "description": "Running count of successful debug bundle generations", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/debug_bundle/probe.cc", + "line": 48 + } + ] + }, + "failed_generation_count": { + "name": "failed_generation_count", + "type": "counter", + "description": "Running count of failed debug bundle generations", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/debug_bundle/probe.cc", + "line": 55 + } + ] + }, + "enterprise_license_expiry_sec": { + "name": "enterprise_license_expiry_sec", + "type": "gauge", + "description": "Number of seconds remaining until the ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/features/feature_table.cc", + "line": 278 + } + ] + }, + "total_puts": { + "name": "total_puts", + "type": "counter", + "description": "Number of completed PUT requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 41 + } + ] + }, + "total_gets": { + "name": "total_gets", + "type": "counter", + "description": "Number of completed GET requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 47 + } + ] + }, + "total_requests": { + "name": "total_requests", + "type": "counter", + "description": "Number of completed HTTP requests (includes PUT and GET)", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 53 + } + ] + }, + "active_puts": { + "name": "active_puts", + "type": "gauge", + "description": "Number of active PUT requests at the moment", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 60 + } + ] + }, + "active_gets": { + "name": "active_gets", + "type": "gauge", + "description": "Number of active GET requests at the moment", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 66 + } + ] + }, + "num_request_timeouts": { + "name": "num_request_timeouts", + "type": "counter", + "description": "Total number of catalog requests that could no longer be retried ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 99 + } + ] + }, + "num_oauth_token_requests": { + "name": "num_oauth_token_requests", + "type": "counter", + "description": "Total number of requests sent to the oauth_token endpoint", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 107 + } + ] + }, + "num_oauth_token_requests_failed": { + "name": "num_oauth_token_requests_failed", + "type": "counter", + "description": "Number of requests sent to the oauth_token endpoint that failed", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 114 + } + ] + }, + "num_create_namespace_requests": { + "name": "num_create_namespace_requests", + "type": "counter", + "description": "Total number of requests sent to the create_namespace endpoint", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 121 + } + ] + }, + "num_create_namespace_requests_failed": { + "name": "num_create_namespace_requests_failed", + "type": "counter", + "description": "Number of requests sent to the create_namespace ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 128 + } + ] + }, + "num_create_table_requests": { + "name": "num_create_table_requests", + "type": "counter", + "description": "Total number of requests sent to the create_table endpoint", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 135 + } + ] + }, + "num_create_table_requests_failed": { + "name": "num_create_table_requests_failed", + "type": "counter", + "description": "Number of requests sent to the create_table endpoint that failed", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 142 + } + ] + }, + "num_load_table_requests": { + "name": "num_load_table_requests", + "type": "counter", + "description": "Total number of requests sent to the load_table endpoint", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 149 + } + ] + }, + "num_load_table_requests_failed": { + "name": "num_load_table_requests_failed", + "type": "counter", + "description": "Number of requests sent to the load_table endpoint that failed", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 156 + } + ] + }, + "num_drop_table_requests": { + "name": "num_drop_table_requests", + "type": "counter", + "description": "Total number of requests sent to the drop_table endpoint", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 163 + } + ] + }, + "num_drop_table_requests_failed": { + "name": "num_drop_table_requests_failed", + "type": "counter", + "description": "Number of requests sent to the drop_table endpoint that failed", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 170 + } + ] + }, + "num_commit_table_update_requests": { + "name": "num_commit_table_update_requests", + "type": "counter", + "description": "Total number of requests sent to the ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 177 + } + ] + }, + "num_commit_table_update_requests_failed": { + "name": "num_commit_table_update_requests_failed", + "type": "counter", + "description": "Number of requests sent to the commit_table_update ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 184 + } + ] + }, + "num_get_config_requests": { + "name": "num_get_config_requests", + "type": "counter", + "description": "Total number of requests sent to the ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 191 + } + ] + }, + "num_get_config_requests_failed": { + "name": "num_get_config_requests_failed", + "type": "counter", + "description": "Number of requests sent to the config ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", + "line": 198 + } + ] + }, + "total_throttle": { + "name": "total_throttle", + "type": "counter", + "description": "Total datalake producer throttle time in milliseconds", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/datalake_throttle_manager.cc", + "line": 266 + } + ] + }, + "throttled_requests": { + "name": "throttled_requests", + "type": "counter", + "description": "Number of requests throttled", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/datalake_throttle_manager.cc", + "line": 271 + } + ] + }, + "delay_seconds_total": { + "name": "delay_seconds_total", + "type": "counter", + "description": "A running total of fetch delay set by the pid controller.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/fetch_pid_controller.cc", + "line": 151 + } + ] + }, + "error_total": { + "name": "error_total", + "type": "counter", + "description": "A running total of error in the fetch PID controller.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/fetch_pid_controller.cc", + "line": 156 + } + ] + }, + "mem_usage_bytes": { + "name": "mem_usage_bytes", + "type": "gauge", + "description": "Fetch sessions cache memory usage in bytes", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/fetch_session_cache.cc", + "line": 180 + } + ] + }, + "sessions_count": { + "name": "sessions_count", + "type": "gauge", + "description": "Total number of fetch sessions", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/fetch_session_cache.cc", + "line": 184 + } + ] + }, + "client_quota_throttle_time": { + "name": "client_quota_throttle_time", + "type": "histogram", + "description": "Client quota throttling delay per rule and ", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/quota_manager.cc", + "line": 69 + } + ] + }, + "client_quota_throughput": { + "name": "client_quota_throughput", + "type": "histogram", + "description": "Client quota throughput per rule and quota type", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/quota_manager.cc", + "line": 80 + } + ] + }, + "fetch_avail_mem_bytes": { + "name": "fetch_avail_mem_bytes", + "type": "counter", + "description": "{}: Memory available for fetch request processing", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/server.cc", + "line": 226 + } + ] + }, + "traffic_intake": { + "name": "traffic_intake", + "type": "counter", + "description": "Amount of Kafka traffic received from the clients ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/snc_quota_manager.cc", + "line": 91 + } + ] + }, + "traffic_egress": { + "name": "traffic_egress", + "type": "counter", + "description": "Amount of Kafka traffic published to the clients ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/snc_quota_manager.cc", + "line": 97 + } + ] + }, + "throttle_time": { + "name": "throttle_time", + "type": "histogram", + "description": "Throttle time histogram (in seconds)", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/snc_quota_manager.cc", + "line": 103 + } + ] + }, + "requests_errored_total": { + "name": "requests_errored_total", + "type": "counter", + "description": "Number of kafka requests errored", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", + "line": 78 + } + ] + }, + "requests_in_progress_total": { + "name": "requests_in_progress_total", + "type": "counter", + "description": "A running total of kafka requests in progress", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", + "line": 83 + } + ] + }, + "received_bytes_total": { + "name": "received_bytes_total", + "type": "counter", + "description": "Number of bytes received from kafka requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", + "line": 88 + } + ] + }, + "sent_bytes_total": { + "name": "sent_bytes_total", + "type": "counter", + "description": "Number of bytes sent in kafka replies", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", + "line": 93 + } + ] + }, + "latency_microseconds": { + "name": "latency_microseconds", + "type": "histogram", + "description": "Latency histogram of kafka requests", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", + "line": 98 + } + ] + }, + "requests_completed_total": { + "name": "requests_completed_total", + "type": "counter", + "description": "Number of kafka requests completed", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", + "line": 112 + } + ] + }, + "latency_seconds": { + "name": "latency_seconds", + "type": "histogram", + "description": "Latency histogram of kafka requests", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", + "line": 137 + }, + { + "file": "tmp/redpanda-dev/src/v/net/server.cc", + "line": 362 + }, + { + "file": "tmp/redpanda-dev/src/v/security/oidc_service.cc", + "line": 94 + } + ] + }, + "Host diskstat {}": { + "name": "Host diskstat {}", + "type": "gauge", + "description": "", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", + "line": 118 + } + ] + }, + "packets_received": { + "name": "packets_received", + "type": "counter", + "description": "Host IP packets received", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", + "line": 156 + } + ] + }, + "packets_sent": { + "name": "packets_sent", + "type": "counter", + "description": "Host IP packets sent", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", + "line": 163 + } + ] + }, + "tcp_established": { + "name": "tcp_established", + "type": "gauge", + "description": "Host TCP established connections", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", + "line": 170 + } + ] + }, + "active_connections": { + "name": "active_connections", + "type": "gauge", + "description": "{}: Currently active connections", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 44 + } + ] + }, + "connects": { + "name": "connects", + "type": "counter", + "description": "{}: Number of accepted connections", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 49 + } + ] + }, + "connection_close_errors": { + "name": "connection_close_errors", + "type": "counter", + "description": "{}: Number of errors when shutting down the connection", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 54 + } + ] + }, + "connections_rejected": { + "name": "connections_rejected", + "type": "counter", + "description": "{}: Number of connection attempts rejected for hitting open ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 59 + } + ] + }, + "connections_rejected_rate_limit": { + "name": "connections_rejected_rate_limit", + "type": "counter", + "description": "{}: Number of connection attempts rejected for hitting ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 66 + } + ] + }, + "requests_completed": { + "name": "requests_completed", + "type": "counter", + "description": "{}: Number of successful requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 73 + } + ] + }, + "received_bytes": { + "name": "received_bytes", + "type": "counter", + "description": "{}: Number of bytes received from the clients in valid requests", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 78 + } + ] + }, + "sent_bytes": { + "name": "sent_bytes", + "type": "counter", + "description": "{}: Number of bytes sent to clients", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 84 + } + ] + }, + "method_not_found_errors": { + "name": "method_not_found_errors", + "type": "counter", + "description": "{}: Number of requests with not available RPC method", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 89 + } + ] + }, + "corrupted_headers": { + "name": "corrupted_headers", + "type": "counter", + "description": "{}: Number of requests with corrupted headers", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 94 + }, + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 546 + } + ] + }, + "service_errors": { + "name": "service_errors", + "type": "counter", + "description": "{}: Number of service errors", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 99 + } + ] + }, + "requests_blocked_memory": { + "name": "requests_blocked_memory", + "type": "counter", + "description": "{}: Number of requests blocked in memory backpressure", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 103 + }, + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 570 + } + ] + }, + "requests_pending": { + "name": "requests_pending", + "type": "gauge", + "description": "{}: Number of requests pending in the queue", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 108 + }, + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 502 + } + ] + }, + "connections_wait_rate": { + "name": "connections_wait_rate", + "type": "counter", + "description": "{}: Number of connections are blocked by connection rate", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 113 + } + ] + }, + "request_errors_total": { + "name": "request_errors_total", + "type": "counter", + "description": "Number of rpc errors", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 137 + }, + { + "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", + "line": 84 + } + ] + }, + "connection_errors": { + "name": "connection_errors", + "type": "counter", + "description": "Number of connection errors", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 209 + } + ] + }, + "truststore_expires_at_timestamp_seconds": { + "name": "truststore_expires_at_timestamp_seconds", + "type": "gauge", + "description": "Expiry time of the shortest-lived CA in the truststore", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 358 + } + ] + }, + "certificate_expires_at_timestamp_seconds": { + "name": "certificate_expires_at_timestamp_seconds", + "type": "gauge", + "description": "Expiry time of the server certificate (seconds since epoch)", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 369 + } + ] + }, + "certificate_serial": { + "name": "certificate_serial", + "type": "gauge", + "description": "Least significant four bytes of the server ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 379 + } + ] + }, + "loaded_at_timestamp_seconds": { + "name": "loaded_at_timestamp_seconds", + "type": "gauge", + "description": "Load time of the server certificate (seconds since epoch).", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 387 + } + ] + }, + "certificate_valid": { + "name": "certificate_valid", + "type": "gauge", + "description": "The value is one if the certificate is valid with ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 395 + } + ] + }, + "trust_file_crc32c": { + "name": "trust_file_crc32c", + "type": "gauge", + "description": "crc32c calculated from the contents of the trust ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/probes.cc", + "line": 403 + } + ] + }, + "max_service_mem_bytes": { + "name": "max_service_mem_bytes", + "type": "counter", + "description": "{}: Maximum memory allowed for RPC", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/server.cc", + "line": 333 + } + ] + }, + "consumed_mem_bytes": { + "name": "consumed_mem_bytes", + "type": "counter", + "description": "{}: Memory consumed by request processing", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/server.cc", + "line": 338 + } + ] + }, + "dispatch_handler_latency": { + "name": "dispatch_handler_latency", + "type": "histogram", + "description": "{}: Latency ", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/net/server.cc", + "line": 343 + } + ] + }, + "request_latency": { + "name": "request_latency", + "type": "histogram", + "description": "Request latency", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", + "line": 61 + } + ] + }, + "request_latency_seconds": { + "name": "request_latency_seconds", + "type": "histogram", + "description": "Internal latency of request for {}", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", + "line": 72 + } + ] + }, + "inflight_requests_usage_ratio": { + "name": "inflight_requests_usage_ratio", + "type": "gauge", + "description": "Usage ratio of in-flight requests in the {}", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", + "line": 165 + } + ] + }, + "inflight_requests_memory_usage_ratio": { + "name": "inflight_requests_memory_usage_ratio", + "type": "gauge", + "description": "Memory usage ratio of in-flight requests in the {}", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", + "line": 174 + } + ] + }, + "queued_requests_memory_blocked": { + "name": "queued_requests_memory_blocked", + "type": "gauge", + "description": "Number of requests queued in {}, due to memory limitations", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", + "line": 184 + } + ] + }, + "inflight_requests": { + "name": "inflight_requests", + "type": "gauge", + "description": "Number of append entries requests that were sent to ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/buffered_protocol.cc", + "line": 378 + } + ] + }, + "buffered_bytes": { + "name": "buffered_bytes", + "type": "gauge", + "description": "Total size of append entries requests in the queue", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/buffered_protocol.cc", + "line": 384 + } + ] + }, + "buffered_requests": { + "name": "buffered_requests", + "type": "gauge", + "description": "Total number of append entries requests in the queue", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/buffered_protocol.cc", + "line": 389 + } + ] + }, + "leader_for": { + "name": "leader_for", + "type": "gauge", + "description": "Number of groups for which node is a leader", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/consensus.cc", + "line": 180 + } + ] + }, + "configuration_change_in_progress": { + "name": "configuration_change_in_progress", + "type": "gauge", + "description": "Indicates if current raft group configuration is in ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/consensus.cc", + "line": 185 + } + ] + }, + "partition_movement_available_bandwidth": { + "name": "partition_movement_available_bandwidth", + "type": "gauge", + "description": "Bandwidth available for partition movement. bytes/sec", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/coordinated_recovery_throttle.cc", + "line": 80 + } + ] + }, + "partition_movement_assigned_bandwidth": { + "name": "partition_movement_assigned_bandwidth", + "type": "gauge", + "description": "Bandwidth assigned for partition movement in last ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/coordinated_recovery_throttle.cc", + "line": 86 + } + ] + }, + "partition_movement_consumed_bandwidth": { + "name": "partition_movement_consumed_bandwidth", + "type": "gauge", + "description": "Bandwidth consumed for partition movement. bytes/sec", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/coordinated_recovery_throttle.cc", + "line": 106 + } + ] + }, + "group_count": { + "name": "group_count", + "type": "gauge", + "description": "Number of raft groups", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/group_manager.cc", + "line": 218 + } + ] + }, + "learners_gap_bytes": { + "name": "learners_gap_bytes", + "type": "gauge", + "description": "Total numbers of bytes that must be delivered to learners", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/group_manager.cc", + "line": 222 + } + ] + }, + "leadership_changes": { + "name": "leadership_changes", + "type": "counter", + "description": "Number of won leader elections across all partitions ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 46 + } + ] + }, + "received_vote_requests": { + "name": "received_vote_requests", + "type": "counter", + "description": "Number of vote requests received", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 62 + } + ] + }, + "received_append_requests": { + "name": "received_append_requests", + "type": "counter", + "description": "Number of append requests received", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 67 + } + ] + }, + "sent_vote_requests": { + "name": "sent_vote_requests", + "type": "counter", + "description": "Number of vote requests sent", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 72 + } + ] + }, + "replicate_ack_all_requests": { + "name": "replicate_ack_all_requests", + "type": "counter", + "description": "Number of replicate requests with quorum ack ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 77 + } + ] + }, + "replicate_ack_all_requests_no_flush": { + "name": "replicate_ack_all_requests_no_flush", + "type": "counter", + "description": "Number of replicate requests with quorum ack ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 83 + } + ] + }, + "replicate_ack_leader_requests": { + "name": "replicate_ack_leader_requests", + "type": "counter", + "description": "Number of replicate requests with leader ack consistency", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 89 + } + ] + }, + "replicate_ack_none_requests": { + "name": "replicate_ack_none_requests", + "type": "counter", + "description": "Number of replicate requests with no ack consistency", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 95 + } + ] + }, + "done_replicate_requests": { + "name": "done_replicate_requests", + "type": "counter", + "description": "Number of finished replicate requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 101 + } + ] + }, + "log_flushes": { + "name": "log_flushes", + "type": "counter", + "description": "Number of log flushes", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 106 + } + ] + }, + "log_truncations": { + "name": "log_truncations", + "type": "counter", + "description": "Number of log truncations", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 111 + } + ] + }, + "replicate_request_errors": { + "name": "replicate_request_errors", + "type": "counter", + "description": "Number of failed replicate requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 121 + } + ] + }, + "heartbeat_requests_errors": { + "name": "heartbeat_requests_errors", + "type": "counter", + "description": "Number of failed heartbeat requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 126 + } + ] + }, + "recovery_requests_errors": { + "name": "recovery_requests_errors", + "type": "counter", + "description": "Number of failed recovery requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 131 + } + ] + }, + "recovery_requests": { + "name": "recovery_requests", + "type": "counter", + "description": "Number of recovery requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 136 + } + ] + }, + "group_configuration_updates": { + "name": "group_configuration_updates", + "type": "counter", + "description": "Number of raft group configuration updates", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 141 + } + ] + }, + "replicate_batch_flush_requests": { + "name": "replicate_batch_flush_requests", + "type": "counter", + "description": "Number of replicate batch flushes", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 146 + } + ] + }, + "lightweight_heartbeat_requests": { + "name": "lightweight_heartbeat_requests", + "type": "counter", + "description": "Number of lightweight heartbeats sent by the leader", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 151 + } + ] + }, + "full_heartbeat_requests": { + "name": "full_heartbeat_requests", + "type": "counter", + "description": "Number of full heartbeats sent by the leader", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 157 + } + ] + }, + "offset_translator_inconsistency_errors": { + "name": "offset_translator_inconsistency_errors", + "type": "counter", + "description": "Number of append entries requests that failed the ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 162 + } + ] + }, + "append_entries_buffer_flushes": { + "name": "append_entries_buffer_flushes", + "type": "counter", + "description": "Number of append entries buffer flushes", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/probe.cc", + "line": 168 + } + ] + }, + "partitions_to_recover": { + "name": "partitions_to_recover", + "type": "gauge", + "description": "Number of partition replicas that have to ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/recovery_scheduler.cc", + "line": 372 + } + ] + }, + "partitions_active": { + "name": "partitions_active", + "type": "gauge", + "description": "Number of partition replicas are currently ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/recovery_scheduler.cc", + "line": 379 + } + ] + }, + "offsets_pending": { + "name": "offsets_pending", + "type": "gauge", + "description": "Sum of offsets that partitions on this node ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/raft/recovery_scheduler.cc", + "line": 386 + } + ] + }, + "uptime_seconds_total": { + "name": "uptime_seconds_total", + "type": "gauge", + "description": "Redpanda uptime in seconds", + "labels": [ + "git_revision", + "git_version" + ], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/redpanda/application.cc", + "line": 731 + } + ] + }, + "build": { + "name": "build", + "type": "gauge", + "description": "Redpanda build information", + "labels": [ + "git_revision", + "git_version" + ], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/redpanda/application.cc", + "line": 739 + } + ] + }, + "fips_mode": { + "name": "fips_mode", + "type": "gauge", + "description": "Identifies whether or not Redpanda is ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/redpanda/application.cc", + "line": 745 + } + ] + }, + "busy_seconds_total": { + "name": "busy_seconds_total", + "type": "gauge", + "description": "Total CPU busy time in seconds", + "labels": [ + "git_", + "git_version" + ], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/redpanda/application.cc", + "line": 761 + } + ] + }, + "uptime": { + "name": "uptime", + "type": "gauge", + "description": "Redpanda uptime in milliseconds", + "labels": [ + "git_revision", + "git_version" + ], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/redpanda/application.cc", + "line": 792 + } + ] + }, + "target_disk_size_bytes": { + "name": "target_disk_size_bytes", + "type": "gauge", + "description": "Target maximum number of stored bytes.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 730 + } + ] + }, + "disk_usage_bytes": { + "name": "disk_usage_bytes", + "type": "gauge", + "description": "Total amount of disk usage under control of space management.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 735 + } + ] + }, + "datalake_disk_usage_bytes": { + "name": "datalake_disk_usage_bytes", + "type": "gauge", + "description": "Total amount of disk usage by datalake.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 741 + } + ] + }, + "retention_reclaimable_bytes": { + "name": "retention_reclaimable_bytes", + "type": "gauge", + "description": "Total amount of reclaimable data through standard ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 746 + } + ] + }, + "available_reclaimable_bytes": { + "name": "available_reclaimable_bytes", + "type": "gauge", + "description": "Total amount of available reclaimable data by space ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 752 + } + ] + }, + "local_retention_reclaimable_bytes": { + "name": "local_retention_reclaimable_bytes", + "type": "gauge", + "description": "Total amount of reclaimable data above the local ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 758 + } + ] + }, + "target_excess_bytes": { + "name": "target_excess_bytes", + "type": "gauge", + "description": "Amount of data usage that exceeds target threshold.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 765 + } + ] + }, + "reclaim_local_bytes": { + "name": "reclaim_local_bytes", + "type": "gauge", + "description": "Estimated amount of data above local retention to be ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 770 + } + ] + }, + "reclaim_low_non_hinted_bytes": { + "name": "reclaim_low_non_hinted_bytes", + "type": "gauge", + "description": "Estimated amount of data above the non-hinted low-space ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 776 + } + ] + }, + "reclaim_low_hinted_bytes": { + "name": "reclaim_low_hinted_bytes", + "type": "gauge", + "description": "Estimated amount of data above the hinted low-space ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 782 + } + ] + }, + "reclaim_active_segment_bytes": { + "name": "reclaim_active_segment_bytes", + "type": "gauge", + "description": "Estimated amount of data above the active segment to be ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 788 + } + ] + }, + "reclaim_estimate_bytes": { + "name": "reclaim_estimate_bytes", + "type": "gauge", + "description": "Estimated amount of data to be reclaimed by space ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", + "line": 794 + } + ] + }, + "requests": { + "name": "requests", + "type": "counter", + "description": "Number of requests", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 495 + } + ] + }, + "request_errors": { + "name": "request_errors", + "type": "counter", + "description": "Number or requests errors", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 509 + } + ] + }, + "request_timeouts": { + "name": "request_timeouts", + "type": "counter", + "description": "Number or requests timeouts", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 516 + } + ] + }, + "out_bytes": { + "name": "out_bytes", + "type": "counter", + "description": "Total number of bytes sent (including headers)", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 524 + } + ] + }, + "in_bytes": { + "name": "in_bytes", + "type": "counter", + "description": "Total number of bytes received", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 531 + } + ] + }, + "read_dispatch_errors": { + "name": "read_dispatch_errors", + "type": "counter", + "description": "Number of errors while dispatching responses", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 538 + } + ] + }, + "server_correlation_errors": { + "name": "server_correlation_errors", + "type": "counter", + "description": "Number of responses with wrong correlation id", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 554 + } + ] + }, + "client_correlation_errors": { + "name": "client_correlation_errors", + "type": "counter", + "description": "Number of errors in client correlation id", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", + "line": 562 + } + ] + }, + "result": { + "name": "result", + "type": "counter", + "description": "Total number of authorization results by type", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/security/authorizer.cc", + "line": 55 + } + ] + }, + "last_event_timestamp_seconds": { + "name": "last_event_timestamp_seconds", + "type": "counter", + "description": "Timestamp of last successful publish on the ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/security/audit/probes.cc", + "line": 32 + } + ] + }, + "buffer_usage_ratio": { + "name": "buffer_usage_ratio", + "type": "gauge", + "description": "Audit event buffer usage ratio.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/security/audit/probes.cc", + "line": 54 + }, + { + "file": "tmp/redpanda-dev/src/v/transform/logging/probes.cc", + "line": 80 + } + ] + }, + "error": { + "name": "error", + "type": "gauge", + "description": "current controller error, i.e difference between ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/backlog_controller.cc", + "line": 140 + } + ] + }, + "shares": { + "name": "shares", + "type": "gauge", + "description": "controller output, i.e. number of shares", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/backlog_controller.cc", + "line": 145 + } + ] + }, + "total_size_bytes": { + "name": "total_size_bytes", + "type": "gauge", + "description": "Total size of all segment appender chunks in any ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/chunk_cache.cc", + "line": 56 + } + ] + }, + "available_size_bytes": { + "name": "available_size_bytes", + "type": "gauge", + "description": "Total size of all free segment appender chunks in ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/chunk_cache.cc", + "line": 61 + } + ] + }, + "wait_count": { + "name": "wait_count", + "type": "counter", + "description": "Count of how many times we had to wait for a chunk ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/chunk_cache.cc", + "line": 66 + } + ] + }, + "segments_rolled": { + "name": "segments_rolled", + "type": "counter", + "description": "Number of segments rolled", + "labels": [], + "constructor": "make_total_operations", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/kvstore.cc", + "line": 70 + } + ] + }, + "entries_fetched": { + "name": "entries_fetched", + "type": "counter", + "description": "Number of entries fetched", + "labels": [], + "constructor": "make_total_operations", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/kvstore.cc", + "line": 74 + } + ] + }, + "entries_written": { + "name": "entries_written", + "type": "counter", + "description": "Number of entries written", + "labels": [], + "constructor": "make_total_operations", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/kvstore.cc", + "line": 78 + } + ] + }, + "entries_removed": { + "name": "entries_removed", + "type": "counter", + "description": "Number of entries removaled", + "labels": [], + "constructor": "make_total_operations", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/kvstore.cc", + "line": 82 + } + ] + }, + "cached_bytes": { + "name": "cached_bytes", + "type": "gauge", + "description": "Size of the database in memory", + "labels": [], + "constructor": "make_current_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/kvstore.cc", + "line": 86 + } + ] + }, + "logs": { + "name": "logs", + "type": "gauge", + "description": "Number of logs managed", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/log_manager_probe.cc", + "line": 31 + } + ] + }, + "urgent_gc_runs": { + "name": "urgent_gc_runs", + "type": "counter", + "description": "Number of urgent GC runs", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/log_manager_probe.cc", + "line": 35 + } + ] + }, + "housekeeping_log_processed": { + "name": "housekeeping_log_processed", + "type": "counter", + "description": "Number of logs processed by housekeeping", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/log_manager_probe.cc", + "line": 39 + } + ] + }, + "total_bytes": { + "name": "total_bytes", + "type": "gauge", + "description": "Total size of attached storage, in bytes.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 50 + } + ] + }, + "free_bytes": { + "name": "free_bytes", + "type": "gauge", + "description": "Disk storage bytes free.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 55 + } + ] + }, + "free_space_alert": { + "name": "free_space_alert", + "type": "gauge", + "description": "Status of low storage space alert. 0-OK, 1-Low Space 2-Degraded", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 60 + } + ] + }, + "written_bytes": { + "name": "written_bytes", + "type": "counter", + "description": "Total number of bytes written", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 113 + } + ] + }, + "batches_written": { + "name": "batches_written", + "type": "counter", + "description": "Total number of batches written", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 118 + } + ] + }, + "cached_read_bytes": { + "name": "cached_read_bytes", + "type": "counter", + "description": "Total number of cached bytes read", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 128 + } + ] + }, + "batches_read": { + "name": "batches_read", + "type": "counter", + "description": "Total number of batches read", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 133 + } + ] + }, + "cached_batches_read": { + "name": "cached_batches_read", + "type": "counter", + "description": "Total number of cached batches read", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 138 + } + ] + }, + "log_segments_created": { + "name": "log_segments_created", + "type": "counter", + "description": "Total number of local log segments created since node startup", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 143 + } + ] + }, + "log_segments_removed": { + "name": "log_segments_removed", + "type": "counter", + "description": "Total number of local log segments removed since node startup", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 149 + } + ] + }, + "log_segments_active": { + "name": "log_segments_active", + "type": "counter", + "description": "Current number of local log segments", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 155 + } + ] + }, + "batch_parse_errors": { + "name": "batch_parse_errors", + "type": "counter", + "description": "Number of batch parsing (reading) errors", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 160 + } + ] + }, + "batch_write_errors": { + "name": "batch_write_errors", + "type": "counter", + "description": "Number of batch write errors", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 165 + } + ] + }, + "corrupted_compaction_indices": { + "name": "corrupted_compaction_indices", + "type": "counter", + "description": "Number of times we had to re-construct the ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 170 + } + ] + }, + "compacted_segment": { + "name": "compacted_segment", + "type": "counter", + "description": "Number of compacted segments", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 176 + } + ] + }, + "partition_size": { + "name": "partition_size", + "type": "gauge", + "description": "Current size of partition in bytes", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 181 + } + ] + }, + "bytes_prefix_truncated": { + "name": "bytes_prefix_truncated", + "type": "counter", + "description": "Number of bytes removed by prefix truncation.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 186 + } + ] + }, + "compaction_removed_bytes": { + "name": "compaction_removed_bytes", + "type": "counter", + "description": "Number of bytes removed by a compaction operation", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 191 + } + ] + }, + "tombstones_removed": { + "name": "tombstones_removed", + "type": "counter", + "description": "Number of tombstone records removed by compaction ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 196 + } + ] + }, + "cleanly_compacted_segment": { + "name": "cleanly_compacted_segment", + "type": "counter", + "description": "Number of segments cleanly compacted (i.e, had their ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 202 + } + ] + }, + "segments_marked_tombstone_free": { + "name": "segments_marked_tombstone_free", + "type": "counter", + "description": "Number of segments that have been verified through ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 210 + } + ] + }, + "complete_sliding_window_rounds": { + "name": "complete_sliding_window_rounds", + "type": "counter", + "description": "Number of rounds of sliding window compaction that ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 216 + } + ] + }, + "chunked_compaction_runs": { + "name": "chunked_compaction_runs", + "type": "counter", + "description": "Number of times chunked compaction was ran. This metric also ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 222 + } + ] + }, + "dirty_segment_bytes": { + "name": "dirty_segment_bytes", + "type": "gauge", + "description": "Number of bytes within dirty segments of the log", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 230 + } + ] + }, + "closed_segment_bytes": { + "name": "closed_segment_bytes", + "type": "gauge", + "description": "Number of bytes within closed segments of the log", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 235 + } + ] + }, + "adjacent_segments_compacted": { + "name": "adjacent_segments_compacted", + "type": "counter", + "description": "Number of segments that have been compacted away ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 240 + } + ] + }, + "compaction_ratio": { + "name": "compaction_ratio", + "type": "counter", + "description": "Average segment compaction ratio", + "labels": [], + "constructor": "make_total_bytes", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 255 + } + ] + }, + "readers_added": { + "name": "readers_added", + "type": "counter", + "description": "Number of readers added to cache", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 291 + } + ] + }, + "readers_evicted": { + "name": "readers_evicted", + "type": "counter", + "description": "Number of readers evicted from cache", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 296 + } + ] + }, + "cache_hits": { + "name": "cache_hits", + "type": "counter", + "description": "Reader cache hits", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 301 + } + ] + }, + "cache_misses": { + "name": "cache_misses", + "type": "counter", + "description": "Reader cache misses", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/storage/probe.cc", + "line": 306 + } + ] + }, + "failures": { + "name": "failures", + "type": "counter", + "description": "The number of transform failures", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/transform/probe.cc", + "line": 38 + } + ] + }, + "lag": { + "name": "lag", + "type": "gauge", + "description": "The number of pending records on the input topic that have ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/transform/probe.cc", + "line": 55 + } + ] + }, + "write_bytes": { + "name": "write_bytes", + "type": "counter", + "description": "The number of bytes output by the transform", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/transform/probe.cc", + "line": 64 + } + ] + }, + "state": { + "name": "state", + "type": "gauge", + "description": "The number of transforms in a specific state", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/transform/probe.cc", + "line": 79 + } + ] + }, + "events_total": { + "name": "events_total", + "type": "counter", + "description": "Running count of transform log events", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/transform/logging/probes.cc", + "line": 32 + } + ] + }, + "events_dropped_total": { + "name": "events_dropped_total", + "type": "counter", + "description": "Running count of dropped transform log events", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/transform/logging/probes.cc", + "line": 37 + } + ] + }, + "write_errors_total": { + "name": "write_errors_total", + "type": "counter", + "description": "Running count of errors while writing to the ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/transform/logging/probes.cc", + "line": 84 + } + ] + }, + "cpu_seconds_total": { + "name": "cpu_seconds_total", + "type": "counter", + "description": "Total CPU time (in seconds) spent inside a WebAssembly ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/wasm/engine_probe.cc", + "line": 38 + } + ] + }, + "memory_usage": { + "name": "memory_usage", + "type": "gauge", + "description": "Amount of memory usage for a WebAssembly function", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/wasm/engine_probe.cc", + "line": 46 + } + ] + }, + "max_memory": { + "name": "max_memory", + "type": "gauge", + "description": "Max amount of memory for a WebAssembly function", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/wasm/engine_probe.cc", + "line": 52 + } + ] + }, + "latency_sec": { + "name": "latency_sec", + "type": "histogram", + "description": "A histogram of the latency in seconds of ", + "labels": [], + "constructor": "make_histogram", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/wasm/transform_probe.cc", + "line": 33 + } + ] + }, + "errors": { + "name": "errors", + "type": "counter", + "description": "Data transform invocation errors", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/wasm/transform_probe.cc", + "line": 40 + } + ] + }, + "executable_memory_usage": { + "name": "executable_memory_usage", + "type": "gauge", + "description": "The amount of executable memory used for ", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/wasm/wasmtime.cc", + "line": 1433 + } + ] + } + }, + "statistics": { + "total_metrics": 378, + "by_type": { + "counter": 223, + "gauge": 141, + "histogram": 14 + }, + "by_constructor": { + "make_counter": 201, + "make_gauge": 140, + "make_total_bytes": 18, + "make_histogram": 14, + "make_total_operations": 4, + "make_current_bytes": 1 + }, + "with_description": 376, + "with_labels": 4 + } +} \ No newline at end of file diff --git a/tools/metrics-extractor/metrics_bag.py b/tools/metrics-extractor/metrics_bag.py new file mode 100644 index 0000000..4214034 --- /dev/null +++ b/tools/metrics-extractor/metrics_bag.py @@ -0,0 +1,170 @@ +import logging +from collections import defaultdict + +logger = logging.getLogger("metrics_bag") + + +class MetricsBag: + """Container for storing and managing extracted metrics""" + + def __init__(self): + self._metrics = {} + + def add_metric(self, name, metric_type, description="", labels=None, + file="", constructor="", line_number=None, group_name=None, full_name=None, **kwargs): + """Add a metric to the bag""" + if labels is None: + labels = [] + + # Create unique key for the metric + key = name + + # If metric already exists, merge information + if key in self._metrics: + existing = self._metrics[key] + + # Update description if current one is empty + if not existing.get("description") and description: + existing["description"] = description + + # Merge labels + existing_labels = set(existing.get("labels", [])) + new_labels = set(labels) + existing["labels"] = sorted(existing_labels | new_labels) + + # Add file location if not already present + files = existing.get("files", []) + file_info = {"file": file, "line": line_number} + if file_info not in files: + files.append(file_info) + existing["files"] = files + else: + # Create new metric entry + metric_data = { + "name": name, + "type": metric_type, + "description": description, + "labels": sorted(labels) if labels else [], + "constructor": constructor, + "files": [{"file": file, "line": line_number}], + "group_name": group_name, + "full_name": full_name + } + + # Add any additional kwargs + metric_data.update(kwargs) + + self._metrics[key] = metric_data + + logger.debug(f"Added/updated metric: {name}") + + def get_metric(self, name): + """Get a specific metric by name""" + return self._metrics.get(name) + + def get_all_metrics(self): + """Get all metrics as a dictionary""" + return self._metrics.copy() + + def get_metrics_by_type(self, metric_type): + """Get all metrics of a specific type""" + return { + name: metric for name, metric in self._metrics.items() + if metric.get("type") == metric_type + } + + def get_metrics_by_constructor(self, constructor): + """Get all metrics created by a specific constructor""" + return { + name: metric for name, metric in self._metrics.items() + if metric.get("constructor") == constructor + } + + def merge(self, other_bag): + """Merge another MetricsBag into this one""" + if not isinstance(other_bag, MetricsBag): + raise ValueError("Can only merge with another MetricsBag instance") + + for name, metric in other_bag.get_all_metrics().items(): + self.add_metric( + name=metric["name"], + metric_type=metric["type"], + description=metric.get("description", ""), + labels=metric.get("labels", []), + file=metric.get("files", [{}])[0].get("file", ""), + constructor=metric.get("constructor", ""), + line_number=metric.get("files", [{}])[0].get("line") + ) + + def filter_by_prefix(self, prefix): + """Get metrics that start with a specific prefix""" + return { + name: metric for name, metric in self._metrics.items() + if name.startswith(prefix) + } + + def get_statistics(self): + """Get statistics about the metrics in the bag""" + stats = { + "total_metrics": len(self._metrics), + "by_type": defaultdict(int), + "by_constructor": defaultdict(int), + "with_description": 0, + "with_labels": 0 + } + + for metric in self._metrics.values(): + stats["by_type"][metric.get("type", "unknown")] += 1 + stats["by_constructor"][metric.get("constructor", "unknown")] += 1 + + if metric.get("description"): + stats["with_description"] += 1 + + if metric.get("labels"): + stats["with_labels"] += 1 + + # Convert defaultdict to regular dict for JSON serialization + stats["by_type"] = dict(stats["by_type"]) + stats["by_constructor"] = dict(stats["by_constructor"]) + + return stats + + def to_dict(self): + """Convert the metrics bag to a dictionary for JSON serialization""" + return { + "metrics": self._metrics, + "statistics": self.get_statistics() + } + + def to_prometheus_format(self): + """Convert metrics to a Prometheus-like format""" + prometheus_metrics = [] + + for name, metric in self._metrics.items(): + prometheus_metric = { + "name": name, + "help": metric.get("description", ""), + "type": metric.get("type", "unknown") + } + + if metric.get("labels"): + prometheus_metric["labels"] = metric["labels"] + + prometheus_metrics.append(prometheus_metric) + + return prometheus_metrics + + def __len__(self): + return len(self._metrics) + + def __iter__(self): + return iter(self._metrics.items()) + + def __contains__(self, name): + return name in self._metrics + + def __getitem__(self, name): + return self._metrics[name] + + def __repr__(self): + return f"MetricsBag({len(self._metrics)} metrics)" diff --git a/tools/metrics-extractor/metrics_extractor.py b/tools/metrics-extractor/metrics_extractor.py new file mode 100644 index 0000000..1d28ad2 --- /dev/null +++ b/tools/metrics-extractor/metrics_extractor.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +import logging +import sys +import os +import json +import re +import argparse +from pathlib import Path +from tree_sitter import Language, Parser +from metrics_parser import build_treesitter_cpp_library, extract_metrics_from_files +from metrics_bag import MetricsBag + +logger = logging.getLogger("metrics_extractor") + + +def validate_paths(options): + path = options.path + + if not os.path.exists(path): + logger.error(f'Path does not exist: "{path}".') + sys.exit(1) + + +def get_cpp_files(options): + """Get all C++ source files from the path""" + path = Path(options.path) + + # If the path is a file, return it directly + if path.is_file() and path.suffix in ['.cc', '.cpp', '.cxx', '.h', '.hpp']: + return [path.resolve()] + + # Otherwise, treat it as a directory + file_patterns = ["*.cc", "*.cpp", "*.cxx"] + cpp_files = [] + + for pattern in file_patterns: + if options.recursive: + cpp_files.extend(path.rglob(pattern)) + else: + cpp_files.extend(path.glob(pattern)) + + return [f.resolve() for f in cpp_files] + + +def get_treesitter_cpp_parser_and_language(treesitter_dir, destination_path): + """Initialize tree-sitter C++ parser and language""" + if not os.path.exists(destination_path): + build_treesitter_cpp_library(treesitter_dir, destination_path) + + cpp_language = Language(destination_path, "cpp") + treesitter_parser = Parser() + treesitter_parser.set_language(cpp_language) + + return treesitter_parser, cpp_language + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Extract Redpanda metrics from C++ source code using tree-sitter" + ) + parser.add_argument( + "path", + help="Path to the Redpanda source code directory" + ) + parser.add_argument( + "--recursive", + "-r", + action="store_true", + help="Search for C++ files recursively" + ) + parser.add_argument( + "--output", + "-o", + default="metrics.json", + help="Output JSON file (default: metrics.json)" + ) + parser.add_argument( + "--asciidoc", + "-a", + help="Generate AsciiDoc output file" + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Enable verbose logging" + ) + parser.add_argument( + "--filter-namespace", + help="Filter metrics by namespace (e.g., redpanda)" + ) + + return parser.parse_args() + + +def generate_asciidoc(metrics_bag, output_file): + """Generate AsciiDoc documentation from metrics""" + with open(output_file, 'w') as f: + f.write("= Redpanda Metrics Reference\n") + f.write(":description: Reference documentation for Redpanda metrics extracted from source code.\n") + f.write(":page-categories: Management, Monitoring\n") + f.write("\n") + f.write("This document lists all metrics found in the Redpanda source code.\n") + f.write("\n") + + # Sort metrics by name + sorted_metrics = sorted(metrics_bag.get_all_metrics().items()) + + for metric_name, metric_info in sorted_metrics: + f.write(f"=== {metric_name}\n\n") + + if metric_info.get('description'): + f.write(f"{metric_info['description']}\n\n") + else: + f.write("No description available.\n\n") + + f.write(f"*Type*: {metric_info.get('type', 'unknown')}\n\n") + + if metric_info.get('labels'): + f.write("*Labels*:\n\n") + for label in sorted(metric_info['labels']): + f.write(f"- `{label}`\n") + f.write("\n") + + if metric_info.get('file'): + f.write(f"*Source*: `{metric_info['file']}`\n\n") + + f.write("---\n\n") + + +def main(): + args = parse_args() + + if args.verbose: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + validate_paths(args) + + logger.info("Initializing tree-sitter C++ parser...") + + # Use the same pattern as property-extractor + treesitter_dir = os.path.join(os.getcwd(), "tree-sitter/tree-sitter-cpp") + destination_path = os.path.join(treesitter_dir, "tree-sitter-cpp.so") + + if not os.path.exists(os.path.join(treesitter_dir, "src/parser.c")): + logger.error("Missing parser.c. Ensure Tree-sitter submodules are initialized.") + logger.error("Run 'make treesitter' first to generate the parser.") + sys.exit(1) + + treesitter_parser, cpp_language = get_treesitter_cpp_parser_and_language( + treesitter_dir, destination_path + ) + + logger.info("Finding C++ source files...") + cpp_files = get_cpp_files(args) + logger.info(f"Found {len(cpp_files)} C++ files") + + logger.info("Extracting metrics from source files...") + metrics_bag = extract_metrics_from_files( + cpp_files, treesitter_parser, cpp_language, args.filter_namespace + ) + + logger.info(f"Extracted {len(metrics_bag.get_all_metrics())} metrics") + + # Output JSON + logger.info(f"Writing JSON output to {args.output}") + with open(args.output, 'w') as f: + json.dump(metrics_bag.to_dict(), f, indent=2) + + # Output AsciiDoc if requested + if args.asciidoc: + logger.info(f"Writing AsciiDoc output to {args.asciidoc}") + generate_asciidoc(metrics_bag, args.asciidoc) + + logger.info("Metrics extraction completed successfully!") + + +if __name__ == "__main__": + main() diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py new file mode 100644 index 0000000..350ff13 --- /dev/null +++ b/tools/metrics-extractor/metrics_parser.py @@ -0,0 +1,315 @@ +import os +import re +import subprocess +import logging +from pathlib import Path +from metrics_bag import MetricsBag + +logger = logging.getLogger("metrics_parser") + +# Tree-sitter queries for different metric constructors +METRICS_QUERIES = { + 'sm_make_gauge': """ + (call_expression + function: (qualified_identifier + scope: (namespace_identifier) @namespace + name: (identifier) @function_name) + arguments: (argument_list + (string_literal) @metric_name + . * + (call_expression + function: (qualified_identifier + scope: (namespace_identifier) + name: (identifier)) + arguments: (argument_list + (string_literal) @description))?)) + """, + + 'ss_metrics_make_current_bytes': """ + (call_expression + function: (qualified_identifier + scope: (qualified_identifier + scope: (namespace_identifier) @outer_namespace + name: (namespace_identifier) @inner_namespace) + name: (identifier) @function_name) + arguments: (argument_list + (string_literal) @metric_name + . * + (call_expression + function: (qualified_identifier + scope: (qualified_identifier + scope: (namespace_identifier) + name: (namespace_identifier)) + name: (identifier)) + arguments: (argument_list + (string_literal) @description))?)) + """ +} + +# Map function names to metric types +FUNCTION_TO_TYPE = { + 'make_gauge': 'gauge', + 'make_counter': 'counter', + 'make_histogram': 'histogram', + 'make_total_bytes': 'counter', + 'make_derive': 'counter', + 'make_total_operations': 'counter', + 'make_current_bytes': 'gauge' +} + + +def build_treesitter_cpp_library(treesitter_dir, destination_path): + """Build tree-sitter C++ library - expects parser to be already generated""" + from tree_sitter import Language + Language.build_library(destination_path, [treesitter_dir]) + + +def get_file_contents(path): + """Read file contents as bytes""" + try: + with open(path, "rb") as f: + return f.read() + except Exception as e: + logger.warning(f"Could not read file {path}: {e}") + return b"" + + +def unquote_string(value): + """Remove quotes from string literals and handle escape sequences""" + if not value: + return "" + + # Remove outer quotes and handle raw strings + value = value.strip() + if value.startswith('R"') and value.endswith('"'): + # Raw string literal: R"delimiter(content)delimiter" + match = re.match(r'R"([^(]*)\((.*)\)\1"', value, re.DOTALL) + if match: + return match.group(2) + elif value.startswith('"') and value.endswith('"'): + # Regular string literal + value = value[1:-1] + # Handle basic escape sequences + value = value.replace('\\"', '"') + value = value.replace('\\\\', '\\') + value = value.replace('\\n', '\n') + value = value.replace('\\t', '\t') + + return value + + +def extract_labels_from_code(code_context): + """Extract potential label names from code context around metrics""" + labels = set() + + # Look for common label patterns + label_patterns = [ + r'\.label\s*\(\s*"([^"]+)"\s*,', + r'labels\s*\{\s*"([^"]+)"\s*,', + r'"([^"]+)"\s*:\s*[^,}]+', # key-value pairs + r'redpanda_([a-z_]+)', # redpanda-specific labels + ] + + for pattern in label_patterns: + matches = re.findall(pattern, code_context) + labels.update(matches) + + return list(labels) + + +def extract_metrics_group_name(call_expr, source_code): + """Extract the metrics group name from add_group call""" + # Look backwards from the current metric to find the add_group call + current_line = call_expr.start_point[0] + source_text = source_code.decode('utf-8', errors='ignore') + lines = source_text.split('\n') + + # Search backwards up to 50 lines for the add_group call + search_start = max(0, current_line - 50) + search_text = '\n'.join(lines[search_start:current_line + 1]) + + # Look for add_group pattern (multi-line support) + add_group_match = re.search( + r'add_group\s*\(\s*prometheus_sanitize::metrics_name\s*\(\s*["\']([^"\']+)["\']', + search_text, + re.MULTILINE | re.DOTALL + ) + if add_group_match: + group_name = add_group_match.group(1) + return group_name + + # Also look for simpler add_group pattern + simple_match = re.search(r'add_group\s*\(\s*["\']([^"\']+)["\']', search_text, re.MULTILINE) + if simple_match: + group_name = simple_match.group(1) + return group_name + + return None + + +def construct_full_metric_name(group_name, metric_name): + """Construct the full Prometheus metric name from group and metric name""" + if not group_name: + return f"redpanda_{metric_name}" + + # Convert group name to Prometheus format + # Replace colons with underscores and ensure redpanda prefix + sanitized_group = group_name.replace(':', '_').replace('-', '_') + + if not sanitized_group.startswith('redpanda_'): + sanitized_group = f"redpanda_{sanitized_group}" + + return f"{sanitized_group}_{metric_name}" + + +def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace=None): + """Parse a single C++ file for metrics definitions""" + logger.debug(f"Parsing file: {file_path}") + + source_code = get_file_contents(file_path) + if not source_code: + return MetricsBag() + + try: + tree = treesitter_parser.parse(source_code) + except Exception as e: + logger.warning(f"Failed to parse {file_path}: {e}") + return MetricsBag() + + metrics_bag = MetricsBag() + + # Simple query to find all call expressions + simple_query = """ + (call_expression + function: (qualified_identifier) @function + arguments: (argument_list) @args) + """ + + try: + query = cpp_language.query(simple_query) + captures = query.captures(tree.root_node) + + for node, label in captures: + if label == "function": + function_text = node.text.decode("utf-8", errors="ignore") + + # Check if this is a metrics function we're interested in + metric_type = None + constructor = None + + if function_text in ["sm::make_gauge", "seastar::metrics::make_gauge"]: + metric_type = "gauge" + constructor = "make_gauge" + elif function_text in ["sm::make_counter", "seastar::metrics::make_counter"]: + metric_type = "counter" + constructor = "make_counter" + elif function_text in ["sm::make_histogram", "seastar::metrics::make_histogram"]: + metric_type = "histogram" + constructor = "make_histogram" + elif function_text in ["sm::make_total_bytes", "seastar::metrics::make_total_bytes"]: + metric_type = "counter" + constructor = "make_total_bytes" + elif function_text in ["sm::make_derive", "seastar::metrics::make_derive"]: + metric_type = "counter" + constructor = "make_derive" + elif function_text in ["ss::metrics::make_total_operations", "seastar::metrics::make_total_operations"]: + metric_type = "counter" + constructor = "make_total_operations" + elif function_text in ["ss::metrics::make_current_bytes", "seastar::metrics::make_current_bytes"]: + metric_type = "gauge" + constructor = "make_current_bytes" + + if metric_type: + # Found a metrics function, now extract the arguments + call_expr = node.parent + if call_expr and call_expr.type == "call_expression": + args_node = None + for child in call_expr.children: + if child.type == "argument_list": + args_node = child + break + + if args_node: + metric_name, description = extract_metric_details(args_node, source_code) + + if metric_name: + # Apply namespace filter if specified + if filter_namespace and not metric_name.startswith(filter_namespace): + continue + + # Try to find the metrics group name by looking for add_group calls + group_name = extract_metrics_group_name(call_expr, source_code) + full_metric_name = construct_full_metric_name(group_name, metric_name) + + # Get code context for labels + start_byte = call_expr.start_byte + end_byte = call_expr.end_byte + context_start = max(0, start_byte - 500) + context_end = min(len(source_code), end_byte + 500) + code_context = source_code[context_start:context_end].decode("utf-8", errors="ignore") + + labels = extract_labels_from_code(code_context) + + metrics_bag.add_metric( + name=metric_name, + metric_type=metric_type, + description=description, + labels=labels, + file=str(file_path.relative_to(Path.cwd()) if file_path.is_absolute() else file_path), + constructor=constructor, + line_number=call_expr.start_point[0] + 1, + group_name=group_name, + full_name=full_metric_name + ) + + logger.debug(f"Found metric: {metric_name} ({metric_type}) -> {full_metric_name}") + + except Exception as e: + logger.warning(f"Query failed on {file_path}: {e}") + + return metrics_bag + + +def extract_metric_details(args_node, source_code): + """Extract metric name and description from argument list""" + metric_name = "" + description = "" + + # Look for string literals in the arguments + string_literals = [] + + def collect_strings(node): + if node.type == "string_literal": + text = node.text.decode("utf-8", errors="ignore") + string_literals.append(unquote_string(text)) + for child in node.children: + collect_strings(child) + + collect_strings(args_node) + + # First string literal is usually the metric name + if string_literals: + metric_name = string_literals[0] + + # Look for description in subsequent strings + for i, string_val in enumerate(string_literals[1:], 1): + if "description" in args_node.text.decode("utf-8", errors="ignore").lower(): + description = string_val + break + + return metric_name, description + + +def extract_metrics_from_files(cpp_files, treesitter_parser, cpp_language, filter_namespace=None): + """Extract metrics from multiple C++ files""" + all_metrics = MetricsBag() + + for file_path in cpp_files: + try: + file_metrics = parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace) + all_metrics.merge(file_metrics) + except Exception as e: + logger.warning(f"Failed to process {file_path}: {e}") + continue + + return all_metrics diff --git a/tools/metrics-extractor/requirements.txt b/tools/metrics-extractor/requirements.txt new file mode 100644 index 0000000..030b250 --- /dev/null +++ b/tools/metrics-extractor/requirements.txt @@ -0,0 +1,2 @@ +tree_sitter==0.21.1 +setuptools>=42.0.0 diff --git a/tools/metrics-extractor/sample.json b/tools/metrics-extractor/sample.json new file mode 100644 index 0000000..a61a895 --- /dev/null +++ b/tools/metrics-extractor/sample.json @@ -0,0 +1,251 @@ +{ + "metrics": { + "puts": { + "name": "puts", + "type": "counter", + "description": "Total number of files put into cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 27 + } + ] + }, + "gets": { + "name": "gets", + "type": "counter", + "description": "Total number of cache get requests.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 31 + } + ] + }, + "cached_gets": { + "name": "cached_gets", + "type": "counter", + "description": "Total number of get requests that are already in cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 35 + } + ] + }, + "size_bytes": { + "name": "size_bytes", + "type": "gauge", + "description": "Current cache size in bytes.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 40 + } + ] + }, + "files": { + "name": "files", + "type": "gauge", + "description": "Current number of files in cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 44 + } + ] + }, + "in_progress_files": { + "name": "in_progress_files", + "type": "gauge", + "description": "Current number of files that are being put to cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 48 + } + ] + }, + "hwm_size_bytes": { + "name": "hwm_size_bytes", + "type": "gauge", + "description": "High watermark of sum of size of cached objects.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 69 + } + ] + }, + "hwm_files": { + "name": "hwm_files", + "type": "gauge", + "description": "High watermark of number of objects in cache.", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 80 + } + ] + }, + "tracker_syncs": { + "name": "tracker_syncs", + "type": "counter", + "description": "Number of times the access tracker was updated ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 86 + } + ] + }, + "tracker_size": { + "name": "tracker_size", + "type": "gauge", + "description": "Number of entries in cache access tracker", + "labels": [], + "constructor": "make_gauge", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 93 + } + ] + }, + "fast_trims": { + "name": "fast_trims", + "type": "counter", + "description": "Number of times we have trimmed the cache ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 103 + } + ] + }, + "exhaustive_trims": { + "name": "exhaustive_trims", + "type": "counter", + "description": "Number of times we couldn't free enough space with a fast ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 109 + } + ] + }, + "carryover_trims": { + "name": "carryover_trims", + "type": "counter", + "description": "Number of times we invoked carryover trim.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 116 + } + ] + }, + "failed_trims": { + "name": "failed_trims", + "type": "counter", + "description": "Number of times could not free the expected amount of ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 121 + } + ] + }, + "in_mem_trims": { + "name": "in_mem_trims", + "type": "counter", + "description": "Number of times we trimmed the cache using ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 128 + } + ] + }, + "put": { + "name": "put", + "type": "counter", + "description": "Number of objects written into cache.", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 141 + } + ] + }, + "hit": { + "name": "hit", + "type": "counter", + "description": "Number of get requests for objects that are ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 146 + } + ] + }, + "miss": { + "name": "miss", + "type": "counter", + "description": "Number of failed get requests because of ", + "labels": [], + "constructor": "make_counter", + "files": [ + { + "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", + "line": 152 + } + ] + } + }, + "statistics": { + "total_metrics": 18, + "by_type": { + "counter": 12, + "gauge": 6 + }, + "by_constructor": { + "make_counter": 12, + "make_gauge": 6 + }, + "with_description": 18, + "with_labels": 0 + } +} \ No newline at end of file diff --git a/tools/metrics-extractor/test_filtered.json b/tools/metrics-extractor/test_filtered.json new file mode 100644 index 0000000..d0f6b48 --- /dev/null +++ b/tools/metrics-extractor/test_filtered.json @@ -0,0 +1,10 @@ +{ + "metrics": {}, + "statistics": { + "total_metrics": 0, + "by_type": {}, + "by_constructor": {}, + "with_description": 0, + "with_labels": 0 + } +} \ No newline at end of file diff --git a/tools/metrics-extractor/tests/test_extraction.py b/tools/metrics-extractor/tests/test_extraction.py new file mode 100644 index 0000000..006b16d --- /dev/null +++ b/tools/metrics-extractor/tests/test_extraction.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +Test sample C++ code to validate metrics extraction +""" + +# Sample C++ code with various metric constructors +SAMPLE_CPP_CODE = ''' +#include + +namespace redpanda { + +class kafka_server { +public: + kafka_server() { + setup_metrics(); + } + +private: + void setup_metrics() { + _metrics.add_group("kafka", { + sm::make_gauge( + "requests_total", + [this] { return _total_requests; }, + sm::description("Total number of Kafka requests processed")), + + sm::make_counter( + "bytes_received_total", + [this] { return _bytes_received; }, + sm::description("Total bytes received from Kafka clients")), + + sm::make_histogram( + "request_latency_seconds", + sm::description("Latency histogram of Kafka requests")), + + sm::make_total_bytes( + "memory_usage_bytes", + [this] { return _memory_used; }, + sm::description("Current memory usage in bytes")), + + ss::metrics::make_total_operations( + "operations_total", + [this] { return _operations; }, + ss::metrics::description("Total operations performed")), + + ss::metrics::make_current_bytes( + "cache_size_bytes", + [this] { return _cache_size; }, + ss::metrics::description("Current cache size in bytes")) + }); + } + + uint64_t _total_requests = 0; + uint64_t _bytes_received = 0; + uint64_t _memory_used = 0; + uint64_t _operations = 0; + uint64_t _cache_size = 0; + ss::metrics::metric_groups _metrics; +}; + +} // namespace redpanda +''' + +def test_sample_extraction(): + """Test that the sample code extracts expected metrics""" + import tempfile + import os + from pathlib import Path + + # Write sample code to temporary file + with tempfile.NamedTemporaryFile(mode='w', suffix='.cc', delete=False) as f: + f.write(SAMPLE_CPP_CODE) + temp_file = f.name + + try: + # Import and test the parser + from metrics_parser import parse_cpp_file, get_treesitter_cpp_parser_and_language + + # Initialize tree-sitter (this will download and compile if needed) + parser, language = get_treesitter_cpp_parser_and_language("tree-sitter", "tree-sitter-cpp.so") + + # Parse the file + metrics_bag = parse_cpp_file(Path(temp_file), parser, language, filter_namespace="redpanda") + + # Check results + all_metrics = metrics_bag.get_all_metrics() + print(f"Found {len(all_metrics)} metrics:") + + expected_metrics = [ + ("requests_total", "gauge"), + ("bytes_received_total", "counter"), + ("request_latency_seconds", "histogram"), + ("memory_usage_bytes", "counter"), + ("operations_total", "counter"), + ("cache_size_bytes", "gauge") + ] + + for metric_name, expected_type in expected_metrics: + if metric_name in all_metrics: + metric = all_metrics[metric_name] + print(f" βœ“ {metric_name} ({metric['type']}) - {metric.get('description', 'No description')}") + assert metric['type'] == expected_type, f"Expected {expected_type}, got {metric['type']}" + else: + print(f" βœ— {metric_name} - NOT FOUND") + + print(f"\nStatistics: {metrics_bag.get_statistics()}") + + finally: + # Clean up + os.unlink(temp_file) + + +if __name__ == "__main__": + test_sample_extraction() diff --git a/tools/metrics-extractor/tree-sitter-cpp.so b/tools/metrics-extractor/tree-sitter-cpp.so new file mode 100644 index 0000000000000000000000000000000000000000..ac87c290dd5c236cb3cc9d474b6473142655637e GIT binary patch literal 3555304 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%0DQ%n1hyJaVwWgQv?8?H}PS4IhEDO{2 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%0D+}U`YtsAY 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

#{~5>6DY{F3Zsf0x^T*}E4}twD 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 literal 0 HcmV?d00001 diff --git a/tools/metrics-extractor/validate.py b/tools/metrics-extractor/validate.py new file mode 100644 index 0000000..c0ac3d9 --- /dev/null +++ b/tools/metrics-extractor/validate.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +Integration script to validate the complete metrics extraction pipeline +""" +import os +import sys +import json +import subprocess +from pathlib import Path + + +def check_dependencies(): + """Check if all required dependencies are available""" + print("πŸ”§ Checking dependencies...") + + try: + import tree_sitter + print(" βœ“ tree-sitter is available") + except ImportError: + print(" ❌ tree-sitter not found. Install with: pip install tree-sitter") + return False + + # Check if we can import our modules + try: + from metrics_parser import build_treesitter_cpp_library, extract_metrics_from_files + from metrics_bag import MetricsBag + print(" βœ“ All custom modules are available") + except ImportError as e: + print(f" ❌ Import error: {e}") + return False + + return True + + +def test_end_to_end(): + """Test the complete extraction pipeline""" + print("\nπŸ§ͺ Running end-to-end test...") + + try: + # Run the example script + result = subprocess.run([ + sys.executable, "example.py", "--verbose" + ], capture_output=True, text=True) + + if result.returncode == 0: + print(" βœ“ Example script completed successfully") + return True + else: + print(f" ❌ Example script failed:") + print(f" stdout: {result.stdout}") + print(f" stderr: {result.stderr}") + return False + except Exception as e: + print(f" ❌ Error running example: {e}") + return False + + +def validate_cli_integration(): + """Validate that the CLI integration works""" + print("\nπŸ”— Validating CLI integration...") + + # Check if the doc-tools.js file has our new command + doc_tools_path = Path("../../bin/doc-tools.js") + if not doc_tools_path.exists(): + print(" ❌ doc-tools.js not found") + return False + + with open(doc_tools_path, 'r') as f: + content = f.read() + + if 'source-metrics-docs' in content: + print(" βœ“ source-metrics-docs command found in doc-tools.js") + else: + print(" ❌ source-metrics-docs command not found in doc-tools.js") + return False + + if 'verifyMetricsExtractorDependencies' in content: + print(" βœ“ verifyMetricsExtractorDependencies function found") + else: + print(" ❌ verifyMetricsExtractorDependencies function not found") + return False + + return True + + +def generate_usage_summary(): + """Generate a summary of how to use the new automation""" + print("\nπŸ“‹ Usage Summary") + print("================") + print() + print("The new Redpanda metrics automation has been successfully created!") + print() + print("πŸ”§ Setup:") + print(" 1. cd tools/metrics-extractor") + print(" 2. make setup-venv") + print(" 3. make install-deps") + print() + print("πŸš€ Usage:") + print(" β€’ Extract from dev branch:") + print(" make build TAG=dev") + print() + print(" β€’ Extract from specific version:") + print(" make build TAG=v23.3.1") + print() + print(" β€’ Extract from local Redpanda repo:") + print(" make extract-local REDPANDA_PATH=/path/to/redpanda") + print() + print(" β€’ CLI integration:") + print(" npx doc-tools generate source-metrics-docs --tag=dev") + print() + print("πŸ“Š Output files:") + print(" β€’ autogenerated/{TAG}/source-metrics/metrics.json") + print(" β€’ autogenerated/{TAG}/source-metrics/metrics.adoc") + print() + print("πŸ†š Comparison with existing metrics:") + print(" python compare_metrics.py autogenerated/dev/source-metrics/metrics.json") + print() + print("πŸ“ Key differences from the current metrics automation:") + print(" β€’ Extracts metrics directly from C++ source code") + print(" β€’ Uses tree-sitter for robust parsing") + print(" β€’ Captures ALL metrics defined in source, not just exposed ones") + print(" β€’ Provides file locations and constructor information") + print(" β€’ Works offline without requiring a running cluster") + print() + print("πŸ” Supported metric constructors:") + print(" β€’ sm::make_gauge") + print(" β€’ sm::make_counter") + print(" β€’ sm::make_histogram") + print(" β€’ sm::make_total_bytes") + print(" β€’ sm::make_derive") + print(" β€’ ss::metrics::make_total_operations") + print(" β€’ ss::metrics::make_current_bytes") + + +def main(): + """Main integration validation""" + print("πŸš€ Redpanda Metrics Extractor Integration Test") + print("===============================================") + + # Change to the metrics-extractor directory + os.chdir(Path(__file__).parent) + + success = True + + # Check dependencies + if not check_dependencies(): + success = False + + # Test end-to-end functionality + if success and not test_end_to_end(): + success = False + + # Validate CLI integration + if not validate_cli_integration(): + print(" ⚠️ CLI integration validation failed, but automation should still work") + + if success: + print("\nπŸŽ‰ All tests passed!") + generate_usage_summary() + return 0 + else: + print("\n❌ Some tests failed. Please check the errors above.") + print("\nFor manual testing:") + print(" python example.py") + return 1 + + +if __name__ == "__main__": + exit(main()) From 2ef97fc77bce2417e11b789927bc4ff00d504caa Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Fri, 18 Jul 2025 20:15:13 -0300 Subject: [PATCH 07/21] continue improving extraction --- tools/metrics-extractor/debug_queries.py | 217 ---------------- tools/metrics-extractor/debug_test.json | 287 --------------------- tools/metrics-extractor/debug_test2.json | 287 --------------------- tools/metrics-extractor/enhanced_test.json | 251 ------------------ tools/metrics-extractor/metrics_bag.py | 42 ++- tools/metrics-extractor/metrics_parser.py | 238 ++++++++++++++--- 6 files changed, 243 insertions(+), 1079 deletions(-) delete mode 100644 tools/metrics-extractor/debug_queries.py delete mode 100644 tools/metrics-extractor/debug_test.json delete mode 100644 tools/metrics-extractor/debug_test2.json delete mode 100644 tools/metrics-extractor/enhanced_test.json diff --git a/tools/metrics-extractor/debug_queries.py b/tools/metrics-extractor/debug_queries.py deleted file mode 100644 index c93418e..0000000 --- a/tools/metrics-extractor/debug_queries.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env python3 -""" -Debug tree-sitter queries to understand why we're not finding metrics -""" -import os -import sys -from pathlib import Path -from tree_sitter import Language, Parser - -# Add the current directory to the path so we can import our modules -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from metrics_parser import METRICS_QUERIES - -# Sample C++ code based on the user's example -SAMPLE_CODE = ''' -#include - -namespace storage { - -class something { -private: - void setup_metrics() { - _metrics.add_group("storage", { - ss::metrics::make_current_bytes( - "cached_bytes", - [this] { return _probe.cached_bytes; }, - ss::metrics::description("Size of the database in memory")), - - sm::make_gauge( - "active_connections", - [this] { return _connections.size(); }, - sm::description("Number of active connections")), - - sm::make_counter( - "requests_total", - [this] { return _total_requests; }, - sm::description("Total number of requests")), - - // More complex cases from user examples - sm::make_counter( - "errors_total", - [this] { return _audit_error_count; }, - sm::description("Running count of errors in creating/publishing " - "audit event log entries")) - .aggregate(aggregate_labels), - - {sm::make_gauge( - "buffer_usage_ratio", - [fn = std::move(get_usage_ratio)] { return fn(); }, - sm::description("Audit client send buffer usage ratio"))}, - - // Test case with different syntax - sm::make_histogram( - "request_duration_ms", - sm::description("Request duration in milliseconds")), - - ss::metrics::make_total_operations( - "disk_operations_total", - [this] { return _disk_ops; }, - ss::metrics::description("Total disk operations performed")) - }); - } - - ss::metrics::metric_groups _metrics; -}; - -} // namespace storage -''' - -def debug_tree_sitter(): - """Debug tree-sitter parsing""" - print("πŸ” Debugging tree-sitter parsing...") - - # Initialize tree-sitter - treesitter_dir = os.path.join(os.getcwd(), "tree-sitter/tree-sitter-cpp") - destination_path = os.path.join(treesitter_dir, "tree-sitter-cpp.so") - - if not os.path.exists(destination_path): - print(f"❌ Tree-sitter library not found at {destination_path}") - print("Run 'make treesitter' first") - return False - - try: - cpp_language = Language(destination_path, "cpp") - parser = Parser() - parser.set_language(cpp_language) - - print("βœ… Tree-sitter initialized successfully") - except Exception as e: - print(f"❌ Failed to initialize tree-sitter: {e}") - return False - - # Parse the sample code - tree = parser.parse(SAMPLE_CODE.encode('utf-8')) - - print(f"\nπŸ“Š Parse tree root: {tree.root_node}") - print(f"πŸ“Š Parse tree text: {tree.root_node.text[:100].decode('utf-8')}...") - - # Test each query - for query_name, query_string in METRICS_QUERIES.items(): - print(f"\nπŸ” Testing query: {query_name}") - print(f"Query: {query_string[:100]}...") - - try: - query = cpp_language.query(query_string) - captures = query.captures(tree.root_node) - - print(f"πŸ“Š Found {len(captures)} captures") - - if captures: - for node, label in captures[:5]: # Show first 5 matches - text = node.text.decode('utf-8', errors='ignore') - print(f" β€’ {label}: {text[:50]}") - - # If this is a metric name, let's see what we got - if label == "metric_name": - print(f" 🎯 FOUND METRIC: {text}") - else: - print(" ⚠️ No captures found") - - except Exception as e: - print(f" ❌ Query failed: {e}") - - # Let's also try a simple query to see if we can find any function calls - print(f"\nπŸ” Testing simple function call query...") - simple_query = """ - (call_expression - function: (qualified_identifier) @function - arguments: (argument_list) @args - ) - """ - - try: - query = cpp_language.query(simple_query) - captures = query.captures(tree.root_node) - print(f"πŸ“Š Found {len(captures)} function calls") - - metrics_found = [] - - for node, label in captures: - if label == "function": - function_text = node.text.decode('utf-8', errors='ignore') - - # Check if this is a metrics function - if any(func in function_text for func in [ - "make_gauge", "make_counter", "make_histogram", - "make_total_bytes", "make_derive", "make_current_bytes", "make_total_operations" - ]): - print(f" β€’ 🎯 METRICS FUNCTION: {function_text}") - - # Get the parent call expression to extract arguments - call_expr = node.parent - if call_expr and call_expr.type == "call_expression": - # Find the argument list - for child in call_expr.children: - if child.type == "argument_list": - args_text = child.text.decode('utf-8', errors='ignore') - print(f" πŸ“ Arguments: {args_text[:150]}...") - - # Extract metric name and description - metric_name = "" - description = "" - string_literals = [] - - # Collect all string literals - def collect_strings(node): - if node.type == "string_literal": - text = node.text.decode('utf-8', errors='ignore') - string_literals.append(text.strip('"')) - for child in node.children: - collect_strings(child) - - collect_strings(child) - - if string_literals: - metric_name = string_literals[0] - print(f" 🏷️ METRIC NAME: '{metric_name}'") - - # Look for description - if len(string_literals) > 1: - for desc in string_literals[1:]: - if len(desc) > 10: # Likely a description - description = desc - print(f" πŸ“„ DESCRIPTION: '{description[:80]}...'") - break - - # Determine metric type - metric_type = "unknown" - if "make_gauge" in function_text or "make_current_bytes" in function_text: - metric_type = "gauge" - elif "make_counter" in function_text or "make_total" in function_text or "make_derive" in function_text: - metric_type = "counter" - elif "make_histogram" in function_text: - metric_type = "histogram" - - print(f" πŸ“Š TYPE: {metric_type}") - - if metric_name: - metrics_found.append((function_text, metric_name, metric_type, description)) - break - else: - print(f" β€’ {function_text}") - - print(f"\nπŸŽ‰ SUMMARY: Found {len(metrics_found)} metrics:") - for func, name, mtype, desc in metrics_found: - print(f" β€’ '{name}' ({mtype}) via {func}") - if desc: - print(f" Description: {desc[:60]}...") - - except Exception as e: - print(f"❌ Simple query failed: {e}") - - return True - -if __name__ == "__main__": - debug_tree_sitter() diff --git a/tools/metrics-extractor/debug_test.json b/tools/metrics-extractor/debug_test.json deleted file mode 100644 index 00d3f12..0000000 --- a/tools/metrics-extractor/debug_test.json +++ /dev/null @@ -1,287 +0,0 @@ -{ - "metrics": { - "puts": { - "name": "puts", - "type": "counter", - "description": "Total number of files put into cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 27 - } - ], - "group_name": null, - "full_name": null - }, - "gets": { - "name": "gets", - "type": "counter", - "description": "Total number of cache get requests.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 31 - } - ], - "group_name": null, - "full_name": null - }, - "cached_gets": { - "name": "cached_gets", - "type": "counter", - "description": "Total number of get requests that are already in cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 35 - } - ], - "group_name": null, - "full_name": null - }, - "size_bytes": { - "name": "size_bytes", - "type": "gauge", - "description": "Current cache size in bytes.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 40 - } - ], - "group_name": null, - "full_name": null - }, - "files": { - "name": "files", - "type": "gauge", - "description": "Current number of files in cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 44 - } - ], - "group_name": null, - "full_name": null - }, - "in_progress_files": { - "name": "in_progress_files", - "type": "gauge", - "description": "Current number of files that are being put to cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 48 - } - ], - "group_name": null, - "full_name": null - }, - "hwm_size_bytes": { - "name": "hwm_size_bytes", - "type": "gauge", - "description": "High watermark of sum of size of cached objects.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 69 - } - ], - "group_name": null, - "full_name": null - }, - "hwm_files": { - "name": "hwm_files", - "type": "gauge", - "description": "High watermark of number of objects in cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 80 - } - ], - "group_name": null, - "full_name": null - }, - "tracker_syncs": { - "name": "tracker_syncs", - "type": "counter", - "description": "Number of times the access tracker was updated ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 86 - } - ], - "group_name": null, - "full_name": null - }, - "tracker_size": { - "name": "tracker_size", - "type": "gauge", - "description": "Number of entries in cache access tracker", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 93 - } - ], - "group_name": null, - "full_name": null - }, - "fast_trims": { - "name": "fast_trims", - "type": "counter", - "description": "Number of times we have trimmed the cache ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 103 - } - ], - "group_name": null, - "full_name": null - }, - "exhaustive_trims": { - "name": "exhaustive_trims", - "type": "counter", - "description": "Number of times we couldn't free enough space with a fast ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 109 - } - ], - "group_name": null, - "full_name": null - }, - "carryover_trims": { - "name": "carryover_trims", - "type": "counter", - "description": "Number of times we invoked carryover trim.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 116 - } - ], - "group_name": null, - "full_name": null - }, - "failed_trims": { - "name": "failed_trims", - "type": "counter", - "description": "Number of times could not free the expected amount of ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 121 - } - ], - "group_name": null, - "full_name": null - }, - "in_mem_trims": { - "name": "in_mem_trims", - "type": "counter", - "description": "Number of times we trimmed the cache using ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 128 - } - ], - "group_name": null, - "full_name": null - }, - "put": { - "name": "put", - "type": "counter", - "description": "Number of objects written into cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 141 - } - ], - "group_name": null, - "full_name": null - }, - "hit": { - "name": "hit", - "type": "counter", - "description": "Number of get requests for objects that are ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 146 - } - ], - "group_name": null, - "full_name": null - }, - "miss": { - "name": "miss", - "type": "counter", - "description": "Number of failed get requests because of ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 152 - } - ], - "group_name": null, - "full_name": null - } - }, - "statistics": { - "total_metrics": 18, - "by_type": { - "counter": 12, - "gauge": 6 - }, - "by_constructor": { - "make_counter": 12, - "make_gauge": 6 - }, - "with_description": 18, - "with_labels": 0 - } -} \ No newline at end of file diff --git a/tools/metrics-extractor/debug_test2.json b/tools/metrics-extractor/debug_test2.json deleted file mode 100644 index 00d3f12..0000000 --- a/tools/metrics-extractor/debug_test2.json +++ /dev/null @@ -1,287 +0,0 @@ -{ - "metrics": { - "puts": { - "name": "puts", - "type": "counter", - "description": "Total number of files put into cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 27 - } - ], - "group_name": null, - "full_name": null - }, - "gets": { - "name": "gets", - "type": "counter", - "description": "Total number of cache get requests.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 31 - } - ], - "group_name": null, - "full_name": null - }, - "cached_gets": { - "name": "cached_gets", - "type": "counter", - "description": "Total number of get requests that are already in cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 35 - } - ], - "group_name": null, - "full_name": null - }, - "size_bytes": { - "name": "size_bytes", - "type": "gauge", - "description": "Current cache size in bytes.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 40 - } - ], - "group_name": null, - "full_name": null - }, - "files": { - "name": "files", - "type": "gauge", - "description": "Current number of files in cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 44 - } - ], - "group_name": null, - "full_name": null - }, - "in_progress_files": { - "name": "in_progress_files", - "type": "gauge", - "description": "Current number of files that are being put to cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 48 - } - ], - "group_name": null, - "full_name": null - }, - "hwm_size_bytes": { - "name": "hwm_size_bytes", - "type": "gauge", - "description": "High watermark of sum of size of cached objects.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 69 - } - ], - "group_name": null, - "full_name": null - }, - "hwm_files": { - "name": "hwm_files", - "type": "gauge", - "description": "High watermark of number of objects in cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 80 - } - ], - "group_name": null, - "full_name": null - }, - "tracker_syncs": { - "name": "tracker_syncs", - "type": "counter", - "description": "Number of times the access tracker was updated ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 86 - } - ], - "group_name": null, - "full_name": null - }, - "tracker_size": { - "name": "tracker_size", - "type": "gauge", - "description": "Number of entries in cache access tracker", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 93 - } - ], - "group_name": null, - "full_name": null - }, - "fast_trims": { - "name": "fast_trims", - "type": "counter", - "description": "Number of times we have trimmed the cache ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 103 - } - ], - "group_name": null, - "full_name": null - }, - "exhaustive_trims": { - "name": "exhaustive_trims", - "type": "counter", - "description": "Number of times we couldn't free enough space with a fast ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 109 - } - ], - "group_name": null, - "full_name": null - }, - "carryover_trims": { - "name": "carryover_trims", - "type": "counter", - "description": "Number of times we invoked carryover trim.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 116 - } - ], - "group_name": null, - "full_name": null - }, - "failed_trims": { - "name": "failed_trims", - "type": "counter", - "description": "Number of times could not free the expected amount of ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 121 - } - ], - "group_name": null, - "full_name": null - }, - "in_mem_trims": { - "name": "in_mem_trims", - "type": "counter", - "description": "Number of times we trimmed the cache using ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 128 - } - ], - "group_name": null, - "full_name": null - }, - "put": { - "name": "put", - "type": "counter", - "description": "Number of objects written into cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 141 - } - ], - "group_name": null, - "full_name": null - }, - "hit": { - "name": "hit", - "type": "counter", - "description": "Number of get requests for objects that are ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 146 - } - ], - "group_name": null, - "full_name": null - }, - "miss": { - "name": "miss", - "type": "counter", - "description": "Number of failed get requests because of ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 152 - } - ], - "group_name": null, - "full_name": null - } - }, - "statistics": { - "total_metrics": 18, - "by_type": { - "counter": 12, - "gauge": 6 - }, - "by_constructor": { - "make_counter": 12, - "make_gauge": 6 - }, - "with_description": 18, - "with_labels": 0 - } -} \ No newline at end of file diff --git a/tools/metrics-extractor/enhanced_test.json b/tools/metrics-extractor/enhanced_test.json deleted file mode 100644 index a61a895..0000000 --- a/tools/metrics-extractor/enhanced_test.json +++ /dev/null @@ -1,251 +0,0 @@ -{ - "metrics": { - "puts": { - "name": "puts", - "type": "counter", - "description": "Total number of files put into cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 27 - } - ] - }, - "gets": { - "name": "gets", - "type": "counter", - "description": "Total number of cache get requests.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 31 - } - ] - }, - "cached_gets": { - "name": "cached_gets", - "type": "counter", - "description": "Total number of get requests that are already in cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 35 - } - ] - }, - "size_bytes": { - "name": "size_bytes", - "type": "gauge", - "description": "Current cache size in bytes.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 40 - } - ] - }, - "files": { - "name": "files", - "type": "gauge", - "description": "Current number of files in cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 44 - } - ] - }, - "in_progress_files": { - "name": "in_progress_files", - "type": "gauge", - "description": "Current number of files that are being put to cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 48 - } - ] - }, - "hwm_size_bytes": { - "name": "hwm_size_bytes", - "type": "gauge", - "description": "High watermark of sum of size of cached objects.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 69 - } - ] - }, - "hwm_files": { - "name": "hwm_files", - "type": "gauge", - "description": "High watermark of number of objects in cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 80 - } - ] - }, - "tracker_syncs": { - "name": "tracker_syncs", - "type": "counter", - "description": "Number of times the access tracker was updated ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 86 - } - ] - }, - "tracker_size": { - "name": "tracker_size", - "type": "gauge", - "description": "Number of entries in cache access tracker", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 93 - } - ] - }, - "fast_trims": { - "name": "fast_trims", - "type": "counter", - "description": "Number of times we have trimmed the cache ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 103 - } - ] - }, - "exhaustive_trims": { - "name": "exhaustive_trims", - "type": "counter", - "description": "Number of times we couldn't free enough space with a fast ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 109 - } - ] - }, - "carryover_trims": { - "name": "carryover_trims", - "type": "counter", - "description": "Number of times we invoked carryover trim.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 116 - } - ] - }, - "failed_trims": { - "name": "failed_trims", - "type": "counter", - "description": "Number of times could not free the expected amount of ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 121 - } - ] - }, - "in_mem_trims": { - "name": "in_mem_trims", - "type": "counter", - "description": "Number of times we trimmed the cache using ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 128 - } - ] - }, - "put": { - "name": "put", - "type": "counter", - "description": "Number of objects written into cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 141 - } - ] - }, - "hit": { - "name": "hit", - "type": "counter", - "description": "Number of get requests for objects that are ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 146 - } - ] - }, - "miss": { - "name": "miss", - "type": "counter", - "description": "Number of failed get requests because of ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 152 - } - ] - } - }, - "statistics": { - "total_metrics": 18, - "by_type": { - "counter": 12, - "gauge": 6 - }, - "by_constructor": { - "make_counter": 12, - "make_gauge": 6 - }, - "with_description": 18, - "with_labels": 0 - } -} \ No newline at end of file diff --git a/tools/metrics-extractor/metrics_bag.py b/tools/metrics-extractor/metrics_bag.py index 4214034..a3d1a03 100644 --- a/tools/metrics-extractor/metrics_bag.py +++ b/tools/metrics-extractor/metrics_bag.py @@ -1,4 +1,6 @@ import logging +import hashlib +import uuid from collections import defaultdict logger = logging.getLogger("metrics_bag") @@ -9,6 +11,15 @@ class MetricsBag: def __init__(self): self._metrics = {} + self._unique_id_counter = 0 + + def _generate_unique_id(self, name, group_name, file_path, line_number): + """Generate a unique ID for a metric based on its properties""" + # Create a deterministic unique ID based on the metric's key properties + key_string = f"{group_name or 'unknown'}::{name}::{file_path}::{line_number}" + # Use SHA256 hash of the key string to create a unique but deterministic ID + hash_object = hashlib.sha256(key_string.encode()) + return hash_object.hexdigest()[:16] # Use first 16 characters for readability def add_metric(self, name, metric_type, description="", labels=None, file="", constructor="", line_number=None, group_name=None, full_name=None, **kwargs): @@ -16,17 +27,29 @@ def add_metric(self, name, metric_type, description="", labels=None, if labels is None: labels = [] - # Create unique key for the metric - key = name + # Generate unique ID for this metric instead of using names as keys + unique_id = self._generate_unique_id(name, group_name, file, line_number) # If metric already exists, merge information - if key in self._metrics: - existing = self._metrics[key] + if unique_id in self._metrics: + existing = self._metrics[unique_id] # Update description if current one is empty if not existing.get("description") and description: existing["description"] = description + # Update group_name and full_name if new values are provided and are not None + # Allow overwriting None values with actual values + if group_name is not None: + existing["group_name"] = group_name + elif "group_name" not in existing: + existing["group_name"] = None + + if full_name is not None: + existing["full_name"] = full_name + elif "full_name" not in existing: + existing["full_name"] = None + # Merge labels existing_labels = set(existing.get("labels", [])) new_labels = set(labels) @@ -54,9 +77,9 @@ def add_metric(self, name, metric_type, description="", labels=None, # Add any additional kwargs metric_data.update(kwargs) - self._metrics[key] = metric_data + self._metrics[unique_id] = metric_data - logger.debug(f"Added/updated metric: {name}") + logger.debug(f"Added/updated metric: {name} with ID: {unique_id}, group_name: {group_name}, full_name: {full_name}") def get_metric(self, name): """Get a specific metric by name""" @@ -93,7 +116,9 @@ def merge(self, other_bag): labels=metric.get("labels", []), file=metric.get("files", [{}])[0].get("file", ""), constructor=metric.get("constructor", ""), - line_number=metric.get("files", [{}])[0].get("line") + line_number=metric.get("files", [{}])[0].get("line"), + group_name=metric.get("group_name"), # Add this + full_name=metric.get("full_name") # Add this ) def filter_by_prefix(self, prefix): @@ -131,8 +156,9 @@ def get_statistics(self): def to_dict(self): """Convert the metrics bag to a dictionary for JSON serialization""" + # Use the unique IDs directly as JSON keys to prevent any conflicts return { - "metrics": self._metrics, + "metrics": self._metrics, # Use unique IDs as keys directly "statistics": self.get_statistics() } diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py index 350ff13..6480b74 100644 --- a/tools/metrics-extractor/metrics_parser.py +++ b/tools/metrics-extractor/metrics_parser.py @@ -104,61 +104,228 @@ def extract_labels_from_code(code_context): # Look for common label patterns label_patterns = [ - r'\.label\s*\(\s*"([^"]+)"\s*,', - r'labels\s*\{\s*"([^"]+)"\s*,', - r'"([^"]+)"\s*:\s*[^,}]+', # key-value pairs - r'redpanda_([a-z_]+)', # redpanda-specific labels + r'\.aggregate\s*\(\s*([^)]+)\s*\)', # .aggregate(aggregate_labels) + r'auto\s+(\w*labels\w*)\s*=', # auto aggregate_labels = + r'std::vector<[^>]*>\s*{([^}]+)}', # std::vector{sm::shard_label} + r'sm::([a-z_]*label[a-z_]*)', # sm::shard_label, sm::topic_label, etc. + r'"([^"]+)"\s*:\s*[^,}]+', # key-value pairs ] for pattern in label_patterns: matches = re.findall(pattern, code_context) - labels.update(matches) + for match in matches: + if isinstance(match, str): + # Clean up label names + cleaned = match.strip().replace('sm::', '').replace('_label', '') + if cleaned and not cleaned.isspace(): + labels.add(cleaned) + elif isinstance(match, tuple): + for submatch in match: + cleaned = submatch.strip().replace('sm::', '').replace('_label', '') + if cleaned and not cleaned.isspace(): + labels.add(cleaned) - return list(labels) + return sorted(list(labels)) + + +def find_group_end_line(group_start_line, lines): + """Find where an add_group block ends by tracking brace nesting""" + # First, we need to find the opening brace of the metrics list + brace_count = 0 + paren_count = 0 + in_group = False + found_opening_brace = False + + for i in range(group_start_line, min(len(lines), group_start_line + 200)): + line = lines[i] + + for j, char in enumerate(line): + if char == '(': + paren_count += 1 + elif char == ')': + paren_count -= 1 + if found_opening_brace and brace_count == 0 and paren_count == 0: + # We've closed all braces and parentheses + return i + elif char == '{': + if not found_opening_brace and paren_count > 0: + # This is the opening brace of the metrics list + found_opening_brace = True + brace_count = 1 + elif found_opening_brace: + brace_count += 1 + elif char == '}': + if found_opening_brace and brace_count > 0: + brace_count -= 1 + if brace_count == 0: + # We've closed the metrics list + # Now look for the closing parenthesis and semicolon + # Could be on this line or the next + remaining = line[j+1:].strip() + if ')' in remaining: + return i + elif i + 1 < len(lines): + next_line = lines[i + 1].strip() + if next_line.startswith(')'): + return i + 1 + + return None def extract_metrics_group_name(call_expr, source_code): - """Extract the metrics group name from add_group call""" - # Look backwards from the current metric to find the add_group call + """Extract the metrics group name from add_group or metric_group calls. + + This function checks if the metric is actually within the bounds of an add_group + block by matching opening and closing braces. + """ current_line = call_expr.start_point[0] + current_column = call_expr.start_point[1] source_text = source_code.decode('utf-8', errors='ignore') lines = source_text.split('\n') - # Search backwards up to 50 lines for the add_group call - search_start = max(0, current_line - 50) - search_text = '\n'.join(lines[search_start:current_line + 1]) - - # Look for add_group pattern (multi-line support) - add_group_match = re.search( - r'add_group\s*\(\s*prometheus_sanitize::metrics_name\s*\(\s*["\']([^"\']+)["\']', - search_text, - re.MULTILINE | re.DOTALL - ) - if add_group_match: - group_name = add_group_match.group(1) - return group_name - - # Also look for simpler add_group pattern - simple_match = re.search(r'add_group\s*\(\s*["\']([^"\']+)["\']', search_text, re.MULTILINE) - if simple_match: - group_name = simple_match.group(1) - return group_name + # Search backwards up to 300 lines for group declarations + search_start = max(0, current_line - 300) + + # Find all potential group declarations with their positions and end lines + all_groups = [] + + for i in range(search_start, current_line + 1): + line = lines[i] + + # Look for prometheus_sanitize::metrics_name patterns first (most accurate) + prometheus_match = re.search( + r'(?:_metrics|_public_metrics)\.add_group\s*\(\s*prometheus_sanitize::metrics_name\s*\(\s*["\']([^"\']+)["\']', + line + ) + if prometheus_match: + end_line = find_group_end_line(i, lines) + if end_line is not None: + all_groups.append({ + 'start': i, + 'end': end_line, + 'name': prometheus_match.group(1), + 'type': "prometheus_add_group" + }) + logger.debug(f"Found group '{prometheus_match.group(1)}' from line {i} to {end_line}") + continue + + # Look for metric_group patterns + metric_group_match = re.search( + r'metric_group\s*\(\s*prometheus_sanitize::metrics_name\s*\(\s*["\']([^"\']+)["\']', + line + ) + if metric_group_match: + end_line = find_group_end_line(i, lines) + if end_line is not None: + all_groups.append({ + 'start': i, + 'end': end_line, + 'name': metric_group_match.group(1), + 'type': "prometheus_metric_group" + }) + continue + + # Fallback to simpler patterns + simple_add_match = re.search(r'\.add_group\s*\(\s*["\']([^"\']+)["\']', line) + if simple_add_match: + end_line = find_group_end_line(i, lines) + if end_line is not None: + all_groups.append({ + 'start': i, + 'end': end_line, + 'name': simple_add_match.group(1), + 'type': "simple_add_group" + }) + continue + + simple_metric_match = re.search(r'metric_group\s*\(\s*["\']([^"\']+)["\']', line) + if simple_metric_match: + end_line = find_group_end_line(i, lines) + if end_line is not None: + all_groups.append({ + 'start': i, + 'end': end_line, + 'name': simple_metric_match.group(1), + 'type': "simple_metric_group" + }) + + # Now find which group contains our metric + # We need to find the innermost group that contains the metric + containing_groups = [] + for group in all_groups: + if group['start'] <= current_line <= group['end']: + containing_groups.append(group) + + if containing_groups: + # Sort by start line (descending) to get the innermost group + containing_groups.sort(key=lambda g: g['start'], reverse=True) + best_group = containing_groups[0] + logger.debug(f"Metric at line {current_line} is in group '{best_group['name']}' (lines {best_group['start']}-{best_group['end']})") + return best_group['name'] + + logger.debug(f"No group name found for metric at line {current_line}") + return None + + +def is_metric_within_group_bounds(group_line, metric_line, metric_column, lines): + """Check if a metric is within the bounds of an add_group call by matching braces. + + This function is now deprecated in favor of find_group_end_line, but kept for reference. + """ + group_end_line = find_group_end_line(group_line, lines) + if group_end_line is not None: + return group_line <= metric_line <= group_end_line + return False + + +def find_metrics_group_context(call_expr, source_code, tree_root): + """Find the metrics group by analyzing the containing function context""" + current_line = call_expr.start_point[0] + + # Find the containing function or method + current_node = call_expr + while current_node: + if current_node.type in ['function_definition', 'method_definition', 'constructor_definition']: + # Found the containing function, search within it + function_text = source_code[current_node.start_byte:current_node.end_byte].decode('utf-8', errors='ignore') + + # Look for add_group calls within this function + add_group_match = re.search( + r'(?:_metrics|_public_metrics)\.add_group\s*\(\s*prometheus_sanitize::metrics_name\s*\(\s*["\']([^"\']+)["\']', + function_text, + re.MULTILINE | re.DOTALL + ) + if add_group_match: + return add_group_match.group(1) + + # Also check for the pattern where add_group might be on multiple lines + multiline_match = re.search( + r'\.add_group\s*\(\s*\n?\s*prometheus_sanitize::metrics_name\s*\(\s*["\']([^"\']+)["\']', + function_text, + re.MULTILINE | re.DOTALL + ) + if multiline_match: + return multiline_match.group(1) + + current_node = current_node.parent return None def construct_full_metric_name(group_name, metric_name): """Construct the full Prometheus metric name from group and metric name""" - if not group_name: + if not group_name or group_name == "unknown": return f"redpanda_{metric_name}" # Convert group name to Prometheus format # Replace colons with underscores and ensure redpanda prefix sanitized_group = group_name.replace(':', '_').replace('-', '_') + # The group name might already have 'redpanda_' prefix or not if not sanitized_group.startswith('redpanda_'): sanitized_group = f"redpanda_{sanitized_group}" + # The full metric name is: _ return f"{sanitized_group}_{metric_name}" @@ -237,10 +404,23 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= if filter_namespace and not metric_name.startswith(filter_namespace): continue + # In parse_cpp_file, update this section: # Try to find the metrics group name by looking for add_group calls group_name = extract_metrics_group_name(call_expr, source_code) + if not group_name: + # Try alternative method + group_name = find_metrics_group_context(call_expr, source_code, tree.root_node) + + if group_name: + logger.debug(f"Found group_name: {group_name} for metric: {metric_name}") + else: + logger.warning(f"Could not find group_name for metric '{metric_name}' at line {call_expr.start_point[0] + 1}") + full_metric_name = construct_full_metric_name(group_name, metric_name) + # Debug output + logger.debug(f"Processing metric '{metric_name}': group_name='{group_name}', full_name='{full_metric_name}'") + # Get code context for labels start_byte = call_expr.start_byte end_byte = call_expr.end_byte @@ -312,4 +492,4 @@ def extract_metrics_from_files(cpp_files, treesitter_parser, cpp_language, filte logger.warning(f"Failed to process {file_path}: {e}") continue - return all_metrics + return all_metrics \ No newline at end of file From 01b869429eeaec468f1b47415862ba5e2d6864f2 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Fri, 18 Jul 2025 20:22:53 -0300 Subject: [PATCH 08/21] working version --- tools/metrics-extractor/metrics_parser.py | 398 +++++++--------------- 1 file changed, 115 insertions(+), 283 deletions(-) diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py index 6480b74..c064dad 100644 --- a/tools/metrics-extractor/metrics_parser.py +++ b/tools/metrics-extractor/metrics_parser.py @@ -128,205 +128,62 @@ def extract_labels_from_code(code_context): return sorted(list(labels)) -def find_group_end_line(group_start_line, lines): - """Find where an add_group block ends by tracking brace nesting""" - # First, we need to find the opening brace of the metrics list - brace_count = 0 - paren_count = 0 - in_group = False - found_opening_brace = False - - for i in range(group_start_line, min(len(lines), group_start_line + 200)): - line = lines[i] - - for j, char in enumerate(line): - if char == '(': - paren_count += 1 - elif char == ')': - paren_count -= 1 - if found_opening_brace and brace_count == 0 and paren_count == 0: - # We've closed all braces and parentheses - return i - elif char == '{': - if not found_opening_brace and paren_count > 0: - # This is the opening brace of the metrics list - found_opening_brace = True - brace_count = 1 - elif found_opening_brace: - brace_count += 1 - elif char == '}': - if found_opening_brace and brace_count > 0: - brace_count -= 1 - if brace_count == 0: - # We've closed the metrics list - # Now look for the closing parenthesis and semicolon - # Could be on this line or the next - remaining = line[j+1:].strip() - if ')' in remaining: - return i - elif i + 1 < len(lines): - next_line = lines[i + 1].strip() - if next_line.startswith(')'): - return i + 1 - - return None - - -def extract_metrics_group_name(call_expr, source_code): - """Extract the metrics group name from add_group or metric_group calls. - - This function checks if the metric is actually within the bounds of an add_group - block by matching opening and closing braces. +def find_group_name_from_ast(metric_call_expr_node): """ - current_line = call_expr.start_point[0] - current_column = call_expr.start_point[1] - source_text = source_code.decode('utf-8', errors='ignore') - lines = source_text.split('\n') - - # Search backwards up to 300 lines for group declarations - search_start = max(0, current_line - 300) - - # Find all potential group declarations with their positions and end lines - all_groups = [] - - for i in range(search_start, current_line + 1): - line = lines[i] - - # Look for prometheus_sanitize::metrics_name patterns first (most accurate) - prometheus_match = re.search( - r'(?:_metrics|_public_metrics)\.add_group\s*\(\s*prometheus_sanitize::metrics_name\s*\(\s*["\']([^"\']+)["\']', - line - ) - if prometheus_match: - end_line = find_group_end_line(i, lines) - if end_line is not None: - all_groups.append({ - 'start': i, - 'end': end_line, - 'name': prometheus_match.group(1), - 'type': "prometheus_add_group" - }) - logger.debug(f"Found group '{prometheus_match.group(1)}' from line {i} to {end_line}") - continue - - # Look for metric_group patterns - metric_group_match = re.search( - r'metric_group\s*\(\s*prometheus_sanitize::metrics_name\s*\(\s*["\']([^"\']+)["\']', - line - ) - if metric_group_match: - end_line = find_group_end_line(i, lines) - if end_line is not None: - all_groups.append({ - 'start': i, - 'end': end_line, - 'name': metric_group_match.group(1), - 'type': "prometheus_metric_group" - }) - continue - - # Fallback to simpler patterns - simple_add_match = re.search(r'\.add_group\s*\(\s*["\']([^"\']+)["\']', line) - if simple_add_match: - end_line = find_group_end_line(i, lines) - if end_line is not None: - all_groups.append({ - 'start': i, - 'end': end_line, - 'name': simple_add_match.group(1), - 'type': "simple_add_group" - }) - continue - - simple_metric_match = re.search(r'metric_group\s*\(\s*["\']([^"\']+)["\']', line) - if simple_metric_match: - end_line = find_group_end_line(i, lines) - if end_line is not None: - all_groups.append({ - 'start': i, - 'end': end_line, - 'name': simple_metric_match.group(1), - 'type': "simple_metric_group" - }) - - # Now find which group contains our metric - # We need to find the innermost group that contains the metric - containing_groups = [] - for group in all_groups: - if group['start'] <= current_line <= group['end']: - containing_groups.append(group) - - if containing_groups: - # Sort by start line (descending) to get the innermost group - containing_groups.sort(key=lambda g: g['start'], reverse=True) - best_group = containing_groups[0] - logger.debug(f"Metric at line {current_line} is in group '{best_group['name']}' (lines {best_group['start']}-{best_group['end']})") - return best_group['name'] - - logger.debug(f"No group name found for metric at line {current_line}") - return None - - -def is_metric_within_group_bounds(group_line, metric_line, metric_column, lines): - """Check if a metric is within the bounds of an add_group call by matching braces. - - This function is now deprecated in favor of find_group_end_line, but kept for reference. + Traverse up the AST from a metric definition to find the enclosing + add_group call and extract its name. This is more reliable than regex. """ - group_end_line = find_group_end_line(group_line, lines) - if group_end_line is not None: - return group_line <= metric_line <= group_end_line - return False + current_node = metric_call_expr_node + while current_node: + # We are looking for a call expression, e.g., _metrics.add_group(...) + if current_node.type == 'call_expression': + function_node = current_node.child_by_field_name('function') + if function_node and function_node.text.decode('utf-8').endswith('.add_group'): + # This is an add_group call. Now, get its arguments. + args_node = current_node.child_by_field_name('arguments') + if not args_node or args_node.named_child_count == 0: + continue + # The first argument should be prometheus_sanitize::metrics_name(...) + first_arg_node = args_node.named_children[0] + + # Check if this argument is a call to prometheus_sanitize::metrics_name + if first_arg_node.type == 'call_expression': + inner_function = first_arg_node.child_by_field_name('function') + inner_args = first_arg_node.child_by_field_name('arguments') + + if inner_function and '::metrics_name' in inner_function.text.decode('utf-8'): + # Found it. Extract the string literal from its arguments. + if inner_args and inner_args.named_child_count > 0: + group_name_node = inner_args.named_children[0] + if group_name_node.type == 'string_literal': + return unquote_string(group_name_node.text.decode('utf-8')) + # Handle simple string literal as group name + elif first_arg_node.type == 'string_literal': + return unquote_string(first_arg_node.text.decode('utf-8')) -def find_metrics_group_context(call_expr, source_code, tree_root): - """Find the metrics group by analyzing the containing function context""" - current_line = call_expr.start_point[0] - - # Find the containing function or method - current_node = call_expr - while current_node: - if current_node.type in ['function_definition', 'method_definition', 'constructor_definition']: - # Found the containing function, search within it - function_text = source_code[current_node.start_byte:current_node.end_byte].decode('utf-8', errors='ignore') - - # Look for add_group calls within this function - add_group_match = re.search( - r'(?:_metrics|_public_metrics)\.add_group\s*\(\s*prometheus_sanitize::metrics_name\s*\(\s*["\']([^"\']+)["\']', - function_text, - re.MULTILINE | re.DOTALL - ) - if add_group_match: - return add_group_match.group(1) - - # Also check for the pattern where add_group might be on multiple lines - multiline_match = re.search( - r'\.add_group\s*\(\s*\n?\s*prometheus_sanitize::metrics_name\s*\(\s*["\']([^"\']+)["\']', - function_text, - re.MULTILINE | re.DOTALL - ) - if multiline_match: - return multiline_match.group(1) - current_node = current_node.parent - return None def construct_full_metric_name(group_name, metric_name): """Construct the full Prometheus metric name from group and metric name""" if not group_name or group_name == "unknown": + # Fallback if group name is not found return f"redpanda_{metric_name}" - # Convert group name to Prometheus format - # Replace colons with underscores and ensure redpanda prefix + # Sanitize the group name: replace special characters with underscores. sanitized_group = group_name.replace(':', '_').replace('-', '_') - # The group name might already have 'redpanda_' prefix or not + # Ensure the 'redpanda' prefix is present. The group name from prometheus_sanitize + # might or might not have it. if not sanitized_group.startswith('redpanda_'): - sanitized_group = f"redpanda_{sanitized_group}" + full_group_name = f"redpanda_{sanitized_group}" + else: + full_group_name = sanitized_group - # The full metric name is: _ - return f"{sanitized_group}_{metric_name}" + # The full metric name is: _ + return f"{full_group_name}_{metric_name}" def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace=None): @@ -345,104 +202,75 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= metrics_bag = MetricsBag() - # Simple query to find all call expressions - simple_query = """ - (call_expression - function: (qualified_identifier) @function - arguments: (argument_list) @args) - """ + # A general query to find all function calls + simple_query = cpp_language.query("(call_expression) @call") try: - query = cpp_language.query(simple_query) - captures = query.captures(tree.root_node) + captures = simple_query.captures(tree.root_node) - for node, label in captures: - if label == "function": - function_text = node.text.decode("utf-8", errors="ignore") - - # Check if this is a metrics function we're interested in - metric_type = None - constructor = None - - if function_text in ["sm::make_gauge", "seastar::metrics::make_gauge"]: - metric_type = "gauge" - constructor = "make_gauge" - elif function_text in ["sm::make_counter", "seastar::metrics::make_counter"]: - metric_type = "counter" - constructor = "make_counter" - elif function_text in ["sm::make_histogram", "seastar::metrics::make_histogram"]: - metric_type = "histogram" - constructor = "make_histogram" - elif function_text in ["sm::make_total_bytes", "seastar::metrics::make_total_bytes"]: - metric_type = "counter" - constructor = "make_total_bytes" - elif function_text in ["sm::make_derive", "seastar::metrics::make_derive"]: - metric_type = "counter" - constructor = "make_derive" - elif function_text in ["ss::metrics::make_total_operations", "seastar::metrics::make_total_operations"]: - metric_type = "counter" - constructor = "make_total_operations" - elif function_text in ["ss::metrics::make_current_bytes", "seastar::metrics::make_current_bytes"]: - metric_type = "gauge" - constructor = "make_current_bytes" - - if metric_type: - # Found a metrics function, now extract the arguments - call_expr = node.parent - if call_expr and call_expr.type == "call_expression": - args_node = None - for child in call_expr.children: - if child.type == "argument_list": - args_node = child - break + for node, _ in captures: + call_expr = node + function_identifier_node = call_expr.child_by_field_name("function") + if not function_identifier_node: + continue + + function_text = function_identifier_node.text.decode("utf-8", errors="ignore") + + metric_type = None + constructor = None + + # Check if this is a metrics function we're interested in + for func, m_type in FUNCTION_TO_TYPE.items(): + if func in function_text: + metric_type = m_type + constructor = func + break + + if metric_type: + # Found a metrics function, now extract its details + args_node = call_expr.child_by_field_name("arguments") + if args_node: + metric_name, description = extract_metric_details(args_node, source_code) + + if metric_name: + # Apply namespace filter if specified + if filter_namespace and not metric_name.startswith(filter_namespace): + continue - if args_node: - metric_name, description = extract_metric_details(args_node, source_code) - - if metric_name: - # Apply namespace filter if specified - if filter_namespace and not metric_name.startswith(filter_namespace): - continue - - # In parse_cpp_file, update this section: - # Try to find the metrics group name by looking for add_group calls - group_name = extract_metrics_group_name(call_expr, source_code) - if not group_name: - # Try alternative method - group_name = find_metrics_group_context(call_expr, source_code, tree.root_node) + # Use robust AST traversal to find the group name + group_name = find_group_name_from_ast(call_expr) - if group_name: - logger.debug(f"Found group_name: {group_name} for metric: {metric_name}") - else: - logger.warning(f"Could not find group_name for metric '{metric_name}' at line {call_expr.start_point[0] + 1}") + if group_name: + logger.debug(f"Found group_name: {group_name} for metric: {metric_name}") + else: + logger.warning(f"Could not find group_name for metric '{metric_name}' at line {call_expr.start_point[0] + 1}") - full_metric_name = construct_full_metric_name(group_name, metric_name) - - # Debug output - logger.debug(f"Processing metric '{metric_name}': group_name='{group_name}', full_name='{full_metric_name}'") - - # Get code context for labels - start_byte = call_expr.start_byte - end_byte = call_expr.end_byte - context_start = max(0, start_byte - 500) - context_end = min(len(source_code), end_byte + 500) - code_context = source_code[context_start:context_end].decode("utf-8", errors="ignore") - - labels = extract_labels_from_code(code_context) - - metrics_bag.add_metric( - name=metric_name, - metric_type=metric_type, - description=description, - labels=labels, - file=str(file_path.relative_to(Path.cwd()) if file_path.is_absolute() else file_path), - constructor=constructor, - line_number=call_expr.start_point[0] + 1, - group_name=group_name, - full_name=full_metric_name - ) - - logger.debug(f"Found metric: {metric_name} ({metric_type}) -> {full_metric_name}") + full_metric_name = construct_full_metric_name(group_name, metric_name) + + logger.debug(f"Processing metric '{metric_name}': group_name='{group_name}', full_name='{full_metric_name}'") + + # Get code context for labels + start_byte = call_expr.start_byte + end_byte = call_expr.end_byte + context_start = max(0, start_byte - 500) + context_end = min(len(source_code), end_byte + 500) + code_context = source_code[context_start:context_end].decode("utf-8", errors="ignore") + + labels = extract_labels_from_code(code_context) + + metrics_bag.add_metric( + name=metric_name, + metric_type=metric_type, + description=description, + labels=labels, + file=str(file_path.relative_to(Path.cwd()) if file_path.is_absolute() else file_path), + constructor=constructor, + line_number=call_expr.start_point[0] + 1, + group_name=group_name, + full_name=full_metric_name + ) + + logger.debug(f"Found metric: {metric_name} ({metric_type}) -> {full_metric_name}") except Exception as e: logger.warning(f"Query failed on {file_path}: {e}") @@ -455,10 +283,10 @@ def extract_metric_details(args_node, source_code): metric_name = "" description = "" - # Look for string literals in the arguments string_literals = [] def collect_strings(node): + # Recursively find all string literals within the arguments if node.type == "string_literal": text = node.text.decode("utf-8", errors="ignore") string_literals.append(unquote_string(text)) @@ -467,16 +295,20 @@ def collect_strings(node): collect_strings(args_node) - # First string literal is usually the metric name + # First string literal is the metric name if string_literals: metric_name = string_literals[0] - # Look for description in subsequent strings - for i, string_val in enumerate(string_literals[1:], 1): - if "description" in args_node.text.decode("utf-8", errors="ignore").lower(): - description = string_val - break - + # The second string literal is usually the description. + # This is a simplification; a more complex heuristic could be needed if + # the description isn't the second string. + if len(string_literals) > 1: + # A simple check to see if the argument list contains "description" + # to be more certain. + args_text = args_node.text.decode("utf-8", errors="ignore") + if "description" in args_text: + description = string_literals[1] + return metric_name, description @@ -492,4 +324,4 @@ def extract_metrics_from_files(cpp_files, treesitter_parser, cpp_language, filte logger.warning(f"Failed to process {file_path}: {e}") continue - return all_metrics \ No newline at end of file + return all_metrics From 412034ae6ab193f973cebdbbe69fb88a03fed664 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Fri, 18 Jul 2025 20:33:38 -0300 Subject: [PATCH 09/21] remove debugs --- tools/metrics-extractor/compare_metrics.py | 186 ------------------- tools/metrics-extractor/metrics_extractor.py | 48 ++++- tools/metrics-extractor/metrics_parser.py | 18 +- 3 files changed, 49 insertions(+), 203 deletions(-) delete mode 100644 tools/metrics-extractor/compare_metrics.py diff --git a/tools/metrics-extractor/compare_metrics.py b/tools/metrics-extractor/compare_metrics.py deleted file mode 100644 index 8204c5d..0000000 --- a/tools/metrics-extractor/compare_metrics.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python3 -""" -Compare extracted metrics with existing metrics documentation -""" -import json -import sys -import argparse -import logging -from pathlib import Path - -logger = logging.getLogger("compare_metrics") - - -def load_extracted_metrics(metrics_file): - """Load metrics from the extracted JSON file""" - try: - with open(metrics_file, 'r') as f: - data = json.load(f) - return data.get('metrics', {}) - except Exception as e: - logger.error(f"Failed to load metrics file {metrics_file}: {e}") - return {} - - -def load_existing_metrics(existing_file): - """Load existing metrics from JSON file if available""" - if not Path(existing_file).exists(): - logger.info(f"No existing metrics file found at {existing_file}") - return {} - - try: - with open(existing_file, 'r') as f: - data = json.load(f) - return data.get('metrics', {}) - except Exception as e: - logger.warning(f"Failed to load existing metrics file {existing_file}: {e}") - return {} - - -def compare_metrics(extracted_metrics, existing_metrics): - """Compare extracted metrics with existing ones""" - extracted_names = set(extracted_metrics.keys()) - existing_names = set(existing_metrics.keys()) - - # Find differences - new_metrics = extracted_names - existing_names - removed_metrics = existing_names - extracted_names - common_metrics = extracted_names & existing_names - - print("=== Metrics Comparison Report ===\n") - - print(f"Total extracted metrics: {len(extracted_names)}") - print(f"Total existing metrics: {len(existing_names)}") - print(f"Common metrics: {len(common_metrics)}") - print(f"New metrics: {len(new_metrics)}") - print(f"Removed metrics: {len(removed_metrics)}\n") - - if new_metrics: - print("πŸ†• NEW METRICS:") - for metric in sorted(new_metrics): - metric_info = extracted_metrics[metric] - print(f" β€’ {metric} ({metric_info.get('type', 'unknown')})") - if metric_info.get('description'): - print(f" Description: {metric_info['description']}") - print() - - if removed_metrics: - print("❌ REMOVED METRICS:") - for metric in sorted(removed_metrics): - print(f" β€’ {metric}") - - if common_metrics: - print("πŸ“Š METRIC TYPE DISTRIBUTION (extracted):") - type_counts = {} - for metric_name in extracted_names: - metric_type = extracted_metrics[metric_name].get('type', 'unknown') - type_counts[metric_type] = type_counts.get(metric_type, 0) + 1 - - for metric_type, count in sorted(type_counts.items()): - print(f" β€’ {metric_type}: {count}") - - print("\nπŸ“ CONSTRUCTOR DISTRIBUTION:") - constructor_counts = {} - for metric_name in extracted_names: - constructor = extracted_metrics[metric_name].get('constructor', 'unknown') - constructor_counts[constructor] = constructor_counts.get(constructor, 0) + 1 - - for constructor, count in sorted(constructor_counts.items()): - print(f" β€’ {constructor}: {count}") - - return { - 'new_metrics': list(new_metrics), - 'removed_metrics': list(removed_metrics), - 'common_metrics': list(common_metrics), - 'total_extracted': len(extracted_names), - 'total_existing': len(existing_names) - } - - -def analyze_metrics_coverage(extracted_metrics): - """Analyze the coverage and quality of extracted metrics""" - print("\n=== Metrics Analysis ===\n") - - total_metrics = len(extracted_metrics) - with_description = sum(1 for m in extracted_metrics.values() if m.get('description')) - with_labels = sum(1 for m in extracted_metrics.values() if m.get('labels')) - - print(f"πŸ“ˆ COVERAGE ANALYSIS:") - print(f" β€’ Total metrics: {total_metrics}") - print(f" β€’ With descriptions: {with_description} ({with_description/total_metrics*100:.1f}%)") - print(f" β€’ With labels: {with_labels} ({with_labels/total_metrics*100:.1f}%)") - - # Analyze by namespace - namespaces = {} - for name, metric in extracted_metrics.items(): - if '_' in name: - namespace = name.split('_')[0] - namespaces[namespace] = namespaces.get(namespace, 0) + 1 - - print(f"\n🏷️ NAMESPACE DISTRIBUTION:") - for namespace, count in sorted(namespaces.items(), key=lambda x: x[1], reverse=True): - print(f" β€’ {namespace}: {count}") - - # Find metrics without descriptions - missing_descriptions = [ - name for name, metric in extracted_metrics.items() - if not metric.get('description') - ] - - if missing_descriptions: - print(f"\n⚠️ METRICS WITHOUT DESCRIPTIONS ({len(missing_descriptions)}):") - for metric in sorted(missing_descriptions)[:10]: # Show first 10 - print(f" β€’ {metric}") - if len(missing_descriptions) > 10: - print(f" ... and {len(missing_descriptions) - 10} more") - - -def main(): - parser = argparse.ArgumentParser(description="Compare extracted metrics") - parser.add_argument("metrics_file", help="JSON file with extracted metrics") - parser.add_argument("--existing", help="Existing metrics JSON file for comparison") - parser.add_argument("--output", help="Output comparison report to JSON file") - parser.add_argument("--verbose", "-v", action="store_true", help="Verbose logging") - - args = parser.parse_args() - - if args.verbose: - logging.basicConfig(level=logging.DEBUG) - else: - logging.basicConfig(level=logging.INFO) - - # Load extracted metrics - extracted_metrics = load_extracted_metrics(args.metrics_file) - if not extracted_metrics: - logger.error("No metrics found in the extracted file") - sys.exit(1) - - # Load existing metrics if provided - existing_metrics = {} - if args.existing: - existing_metrics = load_existing_metrics(args.existing) - - # Perform comparison - if existing_metrics: - comparison_result = compare_metrics(extracted_metrics, existing_metrics) - else: - comparison_result = { - 'new_metrics': list(extracted_metrics.keys()), - 'removed_metrics': [], - 'common_metrics': [], - 'total_extracted': len(extracted_metrics), - 'total_existing': 0 - } - - # Analyze metrics - analyze_metrics_coverage(extracted_metrics) - - # Save comparison result if requested - if args.output: - with open(args.output, 'w') as f: - json.dump(comparison_result, f, indent=2) - logger.info(f"Comparison report saved to {args.output}") - - -if __name__ == "__main__": - main() diff --git a/tools/metrics-extractor/metrics_extractor.py b/tools/metrics-extractor/metrics_extractor.py index 1d28ad2..0c7d0fd 100644 --- a/tools/metrics-extractor/metrics_extractor.py +++ b/tools/metrics-extractor/metrics_extractor.py @@ -5,11 +5,15 @@ import json import re import argparse +import warnings from pathlib import Path from tree_sitter import Language, Parser from metrics_parser import build_treesitter_cpp_library, extract_metrics_from_files from metrics_bag import MetricsBag +# Suppress tree-sitter deprecation warnings +warnings.filterwarnings("ignore", category=FutureWarning, module="tree_sitter") + logger = logging.getLogger("metrics_extractor") @@ -131,14 +135,16 @@ def generate_asciidoc(metrics_bag, output_file): def main(): args = parse_args() + # Set logging level - only show warnings and errors unless verbose is requested if args.verbose: - logging.basicConfig(level=logging.DEBUG) + logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s') else: - logging.basicConfig(level=logging.INFO) + logging.basicConfig(level=logging.WARNING, format='%(levelname)s: %(message)s') validate_paths(args) - logger.info("Initializing tree-sitter C++ parser...") + if args.verbose: + logger.info("Initializing tree-sitter C++ parser...") # Use the same pattern as property-extractor treesitter_dir = os.path.join(os.getcwd(), "tree-sitter/tree-sitter-cpp") @@ -153,28 +159,50 @@ def main(): treesitter_dir, destination_path ) - logger.info("Finding C++ source files...") + if args.verbose: + logger.info("Finding C++ source files...") cpp_files = get_cpp_files(args) - logger.info(f"Found {len(cpp_files)} C++ files") + if args.verbose: + logger.info(f"Found {len(cpp_files)} C++ files") - logger.info("Extracting metrics from source files...") + if args.verbose: + logger.info("Extracting metrics from source files...") metrics_bag = extract_metrics_from_files( cpp_files, treesitter_parser, cpp_language, args.filter_namespace ) - logger.info(f"Extracted {len(metrics_bag.get_all_metrics())} metrics") + # Show clean summary + total_metrics = len(metrics_bag.get_all_metrics()) + print(f"βœ… Successfully extracted {total_metrics} metrics from {len(cpp_files)} C++ files") # Output JSON - logger.info(f"Writing JSON output to {args.output}") + if args.verbose: + logger.info(f"Writing JSON output to {args.output}") with open(args.output, 'w') as f: json.dump(metrics_bag.to_dict(), f, indent=2) # Output AsciiDoc if requested if args.asciidoc: - logger.info(f"Writing AsciiDoc output to {args.asciidoc}") + if args.verbose: + logger.info(f"Writing AsciiDoc output to {args.asciidoc}") generate_asciidoc(metrics_bag, args.asciidoc) - logger.info("Metrics extraction completed successfully!") + print(f"πŸ“„ Output written to: {args.output}") + if args.asciidoc: + print(f"πŸ“„ AsciiDoc written to: {args.asciidoc}") + + # Show breakdown by type + metrics_by_type = {} + for metric_data in metrics_bag.get_all_metrics().values(): + metric_type = metric_data.get('type', 'unknown') + metrics_by_type[metric_type] = metrics_by_type.get(metric_type, 0) + 1 + + if metrics_by_type: + print(f"πŸ“Š Metrics by type:") + for metric_type, count in sorted(metrics_by_type.items()): + print(f" β€’ {metric_type}: {count}") + + print("πŸŽ‰ Metrics extraction completed successfully!") if __name__ == "__main__": diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py index c064dad..a06bbc7 100644 --- a/tools/metrics-extractor/metrics_parser.py +++ b/tools/metrics-extractor/metrics_parser.py @@ -188,7 +188,8 @@ def construct_full_metric_name(group_name, metric_name): def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace=None): """Parse a single C++ file for metrics definitions""" - logger.debug(f"Parsing file: {file_path}") + # Only show debug info in verbose mode + # logger.debug(f"Parsing file: {file_path}") source_code = get_file_contents(file_path) if not source_code: @@ -240,14 +241,16 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= # Use robust AST traversal to find the group name group_name = find_group_name_from_ast(call_expr) - if group_name: - logger.debug(f"Found group_name: {group_name} for metric: {metric_name}") - else: - logger.warning(f"Could not find group_name for metric '{metric_name}' at line {call_expr.start_point[0] + 1}") + # Only show warnings for missing groups in verbose mode + # if group_name: + # logger.debug(f"Found group_name: {group_name} for metric: {metric_name}") + # else: + # logger.warning(f"Could not find group_name for metric '{metric_name}' at line {call_expr.start_point[0] + 1}") full_metric_name = construct_full_metric_name(group_name, metric_name) - logger.debug(f"Processing metric '{metric_name}': group_name='{group_name}', full_name='{full_metric_name}'") + # Commented out to reduce debug noise + # logger.debug(f"Processing metric '{metric_name}': group_name='{group_name}', full_name='{full_metric_name}'") # Get code context for labels start_byte = call_expr.start_byte @@ -270,7 +273,8 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= full_name=full_metric_name ) - logger.debug(f"Found metric: {metric_name} ({metric_type}) -> {full_metric_name}") + # Commented out to reduce noise + # logger.debug(f"Found metric: {metric_name} ({metric_type}) -> {full_metric_name}") except Exception as e: logger.warning(f"Query failed on {file_path}: {e}") From d26ab4e1c170560d68025a0ee27b71e8f6352562 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 22 Jul 2025 15:43:56 -0300 Subject: [PATCH 10/21] change from UUID to full_name --- tools/metrics-extractor/metrics_bag.py | 26 +++++++++++++------- tools/metrics-extractor/metrics_extractor.py | 12 +++++---- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/tools/metrics-extractor/metrics_bag.py b/tools/metrics-extractor/metrics_bag.py index a3d1a03..f2701e4 100644 --- a/tools/metrics-extractor/metrics_bag.py +++ b/tools/metrics-extractor/metrics_bag.py @@ -27,17 +27,24 @@ def add_metric(self, name, metric_type, description="", labels=None, if labels is None: labels = [] - # Generate unique ID for this metric instead of using names as keys + # Generate unique ID for this metric but now use full_name as the key unique_id = self._generate_unique_id(name, group_name, file, line_number) + # Use full_name as the key, fallback to unique_id if full_name is not available + key = full_name if full_name else unique_id + # If metric already exists, merge information - if unique_id in self._metrics: - existing = self._metrics[unique_id] + if key in self._metrics: + existing = self._metrics[key] # Update description if current one is empty if not existing.get("description") and description: existing["description"] = description + # Update unique_id if not present (for backward compatibility) + if "unique_id" not in existing: + existing["unique_id"] = unique_id + # Update group_name and full_name if new values are provided and are not None # Allow overwriting None values with actual values if group_name is not None: @@ -64,6 +71,7 @@ def add_metric(self, name, metric_type, description="", labels=None, else: # Create new metric entry metric_data = { + "unique_id": unique_id, # Add unique_id as a field "name": name, "type": metric_type, "description": description, @@ -77,9 +85,9 @@ def add_metric(self, name, metric_type, description="", labels=None, # Add any additional kwargs metric_data.update(kwargs) - self._metrics[unique_id] = metric_data + self._metrics[key] = metric_data - logger.debug(f"Added/updated metric: {name} with ID: {unique_id}, group_name: {group_name}, full_name: {full_name}") + logger.debug(f"Added/updated metric: {name} with key: {key} (unique_id: {unique_id}), group_name: {group_name}, full_name: {full_name}") def get_metric(self, name): """Get a specific metric by name""" @@ -117,8 +125,8 @@ def merge(self, other_bag): file=metric.get("files", [{}])[0].get("file", ""), constructor=metric.get("constructor", ""), line_number=metric.get("files", [{}])[0].get("line"), - group_name=metric.get("group_name"), # Add this - full_name=metric.get("full_name") # Add this + group_name=metric.get("group_name"), + full_name=metric.get("full_name") ) def filter_by_prefix(self, prefix): @@ -156,9 +164,9 @@ def get_statistics(self): def to_dict(self): """Convert the metrics bag to a dictionary for JSON serialization""" - # Use the unique IDs directly as JSON keys to prevent any conflicts + # Use the full names (or unique IDs as fallback) as JSON keys return { - "metrics": self._metrics, # Use unique IDs as keys directly + "metrics": self._metrics, # Use full names as keys directly "statistics": self.get_statistics() } diff --git a/tools/metrics-extractor/metrics_extractor.py b/tools/metrics-extractor/metrics_extractor.py index 0c7d0fd..b387510 100644 --- a/tools/metrics-extractor/metrics_extractor.py +++ b/tools/metrics-extractor/metrics_extractor.py @@ -107,11 +107,13 @@ def generate_asciidoc(metrics_bag, output_file): f.write("This document lists all metrics found in the Redpanda source code.\n") f.write("\n") - # Sort metrics by name + # Sort metrics by the key (which is now full_name or fallback to unique_id) sorted_metrics = sorted(metrics_bag.get_all_metrics().items()) - for metric_name, metric_info in sorted_metrics: - f.write(f"=== {metric_name}\n\n") + for metric_key, metric_info in sorted_metrics: + # Use full_name as section header, fallback to metric_key if full_name is not available + section_name = metric_info.get('full_name', metric_key) + f.write(f"=== {section_name}\n\n") if metric_info.get('description'): f.write(f"{metric_info['description']}\n\n") @@ -126,8 +128,8 @@ def generate_asciidoc(metrics_bag, output_file): f.write(f"- `{label}`\n") f.write("\n") - if metric_info.get('file'): - f.write(f"*Source*: `{metric_info['file']}`\n\n") + if metric_info.get('files') and metric_info['files']: + f.write(f"*Source*: `{metric_info['files'][0].get('file', '')}`\n\n") f.write("---\n\n") From 1a5d7e007876fc7d064bebc1b55b209be9139f34 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 22 Jul 2025 15:44:08 -0300 Subject: [PATCH 11/21] split internal and external metrics --- tools/metrics-extractor/metrics_bag.py | 17 ++++-- tools/metrics-extractor/metrics_parser.py | 72 +++++++++++++++++------ 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/tools/metrics-extractor/metrics_bag.py b/tools/metrics-extractor/metrics_bag.py index f2701e4..d582042 100644 --- a/tools/metrics-extractor/metrics_bag.py +++ b/tools/metrics-extractor/metrics_bag.py @@ -22,7 +22,8 @@ def _generate_unique_id(self, name, group_name, file_path, line_number): return hash_object.hexdigest()[:16] # Use first 16 characters for readability def add_metric(self, name, metric_type, description="", labels=None, - file="", constructor="", line_number=None, group_name=None, full_name=None, **kwargs): + file="", constructor="", line_number=None, group_name=None, full_name=None, + internal_external_type="external", **kwargs): """Add a metric to the bag""" if labels is None: labels = [] @@ -57,6 +58,12 @@ def add_metric(self, name, metric_type, description="", labels=None, elif "full_name" not in existing: existing["full_name"] = None + # Update internal_external_type + if internal_external_type is not None: + existing["metric_type"] = internal_external_type + elif "metric_type" not in existing: + existing["metric_type"] = "external" # default + # Merge labels existing_labels = set(existing.get("labels", [])) new_labels = set(labels) @@ -79,7 +86,8 @@ def add_metric(self, name, metric_type, description="", labels=None, "constructor": constructor, "files": [{"file": file, "line": line_number}], "group_name": group_name, - "full_name": full_name + "full_name": full_name, + "metric_type": internal_external_type # Add the internal/external classification } # Add any additional kwargs @@ -87,7 +95,7 @@ def add_metric(self, name, metric_type, description="", labels=None, self._metrics[key] = metric_data - logger.debug(f"Added/updated metric: {name} with key: {key} (unique_id: {unique_id}), group_name: {group_name}, full_name: {full_name}") + logger.debug(f"Added/updated metric: {name} with key: {key} (unique_id: {unique_id}), group_name: {group_name}, full_name: {full_name}, metric_type: {internal_external_type}") def get_metric(self, name): """Get a specific metric by name""" @@ -126,7 +134,8 @@ def merge(self, other_bag): constructor=metric.get("constructor", ""), line_number=metric.get("files", [{}])[0].get("line"), group_name=metric.get("group_name"), - full_name=metric.get("full_name") + full_name=metric.get("full_name"), + internal_external_type=metric.get("metric_type", "external") ) def filter_by_prefix(self, prefix): diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py index a06bbc7..6b8de7b 100644 --- a/tools/metrics-extractor/metrics_parser.py +++ b/tools/metrics-extractor/metrics_parser.py @@ -128,17 +128,29 @@ def extract_labels_from_code(code_context): return sorted(list(labels)) -def find_group_name_from_ast(metric_call_expr_node): +def find_group_name_and_type_from_ast(metric_call_expr_node): """ Traverse up the AST from a metric definition to find the enclosing - add_group call and extract its name. This is more reliable than regex. + add_group call and extract its name and metric type (internal/external). + Returns tuple: (group_name, metric_type) """ current_node = metric_call_expr_node while current_node: - # We are looking for a call expression, e.g., _metrics.add_group(...) + # We are looking for a call expression, e.g., _metrics.add_group(...) or _public_metrics.add_group(...) if current_node.type == 'call_expression': function_node = current_node.child_by_field_name('function') if function_node and function_node.text.decode('utf-8').endswith('.add_group'): + function_text = function_node.text.decode('utf-8') + + # Determine metric type based on the object being called + metric_type = "external" # default + if '_metrics.add_group' in function_text and 'public' not in function_text: + # This is likely internal_metric_groups or just _metrics (internal) + metric_type = "internal" + elif '_public_metrics.add_group' in function_text or 'public_metric_groups' in function_text: + # This is public_metric_groups (external) + metric_type = "external" + # This is an add_group call. Now, get its arguments. args_node = current_node.child_by_field_name('arguments') if not args_node or args_node.named_child_count == 0: @@ -157,30 +169,51 @@ def find_group_name_from_ast(metric_call_expr_node): if inner_args and inner_args.named_child_count > 0: group_name_node = inner_args.named_children[0] if group_name_node.type == 'string_literal': - return unquote_string(group_name_node.text.decode('utf-8')) + group_name = unquote_string(group_name_node.text.decode('utf-8')) + return group_name, metric_type # Handle simple string literal as group name elif first_arg_node.type == 'string_literal': - return unquote_string(first_arg_node.text.decode('utf-8')) + group_name = unquote_string(first_arg_node.text.decode('utf-8')) + return group_name, metric_type current_node = current_node.parent - return None + return None, "external" # Default to external if not found + + +def find_group_name_from_ast(metric_call_expr_node): + """ + Traverse up the AST from a metric definition to find the enclosing + add_group call and extract its name. This is more reliable than regex. + """ + group_name, _ = find_group_name_and_type_from_ast(metric_call_expr_node) + return group_name -def construct_full_metric_name(group_name, metric_name): +def construct_full_metric_name(group_name, metric_name, metric_type="external"): """Construct the full Prometheus metric name from group and metric name""" if not group_name or group_name == "unknown": - # Fallback if group name is not found - return f"redpanda_{metric_name}" + # Fallback based on metric type + if metric_type == "internal": + return f"vectorized_{metric_name}" + else: + return f"redpanda_{metric_name}" # Sanitize the group name: replace special characters with underscores. sanitized_group = group_name.replace(':', '_').replace('-', '_') - # Ensure the 'redpanda' prefix is present. The group name from prometheus_sanitize - # might or might not have it. - if not sanitized_group.startswith('redpanda_'): - full_group_name = f"redpanda_{sanitized_group}" + # Ensure the correct prefix is present based on metric type + if metric_type == "internal": + # Internal metrics should have vectorized_ prefix + if not sanitized_group.startswith('vectorized_'): + full_group_name = f"vectorized_{sanitized_group}" + else: + full_group_name = sanitized_group else: - full_group_name = sanitized_group + # External metrics should have redpanda_ prefix + if not sanitized_group.startswith('redpanda_'): + full_group_name = f"redpanda_{sanitized_group}" + else: + full_group_name = sanitized_group # The full metric name is: _ return f"{full_group_name}_{metric_name}" @@ -238,8 +271,8 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= if filter_namespace and not metric_name.startswith(filter_namespace): continue - # Use robust AST traversal to find the group name - group_name = find_group_name_from_ast(call_expr) + # Use robust AST traversal to find the group name and metric type + group_name, internal_external_type = find_group_name_and_type_from_ast(call_expr) # Only show warnings for missing groups in verbose mode # if group_name: @@ -247,10 +280,10 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= # else: # logger.warning(f"Could not find group_name for metric '{metric_name}' at line {call_expr.start_point[0] + 1}") - full_metric_name = construct_full_metric_name(group_name, metric_name) + full_metric_name = construct_full_metric_name(group_name, metric_name, internal_external_type) # Commented out to reduce debug noise - # logger.debug(f"Processing metric '{metric_name}': group_name='{group_name}', full_name='{full_metric_name}'") + # logger.debug(f"Processing metric '{metric_name}': group_name='{group_name}', metric_type='{internal_external_type}', full_name='{full_metric_name}'") # Get code context for labels start_byte = call_expr.start_byte @@ -270,7 +303,8 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= constructor=constructor, line_number=call_expr.start_point[0] + 1, group_name=group_name, - full_name=full_metric_name + full_name=full_metric_name, + internal_external_type=internal_external_type # Add the new field ) # Commented out to reduce noise From 55e622b0dc986797f8ad1cf69681bd14c532107e Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 22 Jul 2025 16:07:19 -0300 Subject: [PATCH 12/21] split file gen between internal and external --- tools/metrics-extractor/Makefile | 6 +- tools/metrics-extractor/metrics_bag.py | 2 - tools/metrics-extractor/metrics_extractor.py | 157 ++++++++++++++----- 3 files changed, 124 insertions(+), 41 deletions(-) diff --git a/tools/metrics-extractor/Makefile b/tools/metrics-extractor/Makefile index 05ea988..c1e304b 100644 --- a/tools/metrics-extractor/Makefile +++ b/tools/metrics-extractor/Makefile @@ -68,7 +68,8 @@ extract-metrics: $(PYTHON) metrics_extractor.py \ --recursive \ --output $(OUTPUT_DIR)/metrics.json \ - --asciidoc $(OUTPUT_DIR)/metrics.adoc \ + --internal-asciidoc $(OUTPUT_DIR)/internal_metrics_reference.adoc \ + --external-asciidoc $(OUTPUT_DIR)/public_metrics_reference.adoc \ --verbose \ $(REDPANDA_DIR)/src @@ -133,7 +134,8 @@ extract-local: $(PYTHON) metrics_extractor.py \ --recursive \ --output $(OUTPUT_DIR)/metrics.json \ - --asciidoc $(OUTPUT_DIR)/metrics.adoc \ + --internal-asciidoc $(OUTPUT_DIR)/internal_metrics_reference.adoc \ + --external-asciidoc $(OUTPUT_DIR)/public_metrics_reference.adoc \ --filter-namespace redpanda \ --verbose \ $(REDPANDA_PATH)/src diff --git a/tools/metrics-extractor/metrics_bag.py b/tools/metrics-extractor/metrics_bag.py index d582042..15e1a12 100644 --- a/tools/metrics-extractor/metrics_bag.py +++ b/tools/metrics-extractor/metrics_bag.py @@ -94,8 +94,6 @@ def add_metric(self, name, metric_type, description="", labels=None, metric_data.update(kwargs) self._metrics[key] = metric_data - - logger.debug(f"Added/updated metric: {name} with key: {key} (unique_id: {unique_id}), group_name: {group_name}, full_name: {full_name}, metric_type: {internal_external_type}") def get_metric(self, name): """Get a specific metric by name""" diff --git a/tools/metrics-extractor/metrics_extractor.py b/tools/metrics-extractor/metrics_extractor.py index b387510..a3574ab 100644 --- a/tools/metrics-extractor/metrics_extractor.py +++ b/tools/metrics-extractor/metrics_extractor.py @@ -78,10 +78,18 @@ def parse_args(): default="metrics.json", help="Output JSON file (default: metrics.json)" ) + parser.add_argument( + "--internal-asciidoc", + help="Generate AsciiDoc output file for internal metrics" + ) + parser.add_argument( + "--external-asciidoc", + help="Generate AsciiDoc output file for external metrics" + ) parser.add_argument( "--asciidoc", "-a", - help="Generate AsciiDoc output file" + help="Generate AsciiDoc output file (deprecated: use --internal-asciidoc and --external-asciidoc)" ) parser.add_argument( "--verbose", @@ -97,41 +105,94 @@ def parse_args(): return parser.parse_args() -def generate_asciidoc(metrics_bag, output_file): - """Generate AsciiDoc documentation from metrics""" - with open(output_file, 'w') as f: - f.write("= Redpanda Metrics Reference\n") - f.write(":description: Reference documentation for Redpanda metrics extracted from source code.\n") - f.write(":page-categories: Management, Monitoring\n") - f.write("\n") - f.write("This document lists all metrics found in the Redpanda source code.\n") - f.write("\n") - - # Sort metrics by the key (which is now full_name or fallback to unique_id) - sorted_metrics = sorted(metrics_bag.get_all_metrics().items()) - - for metric_key, metric_info in sorted_metrics: - # Use full_name as section header, fallback to metric_key if full_name is not available - section_name = metric_info.get('full_name', metric_key) - f.write(f"=== {section_name}\n\n") - - if metric_info.get('description'): - f.write(f"{metric_info['description']}\n\n") - else: - f.write("No description available.\n\n") +def generate_asciidoc_by_type(metrics_bag, internal_output_file, external_output_file): + """Generate separate AsciiDoc documentation for internal and external metrics""" + all_metrics = metrics_bag.get_all_metrics() + + # Separate metrics by type + internal_metrics = {} + external_metrics = {} + + for metric_key, metric_info in all_metrics.items(): + metric_type = metric_info.get('metric_type', 'external') # Default to external if not specified + if metric_type == 'internal': + internal_metrics[metric_key] = metric_info + else: + external_metrics[metric_key] = metric_info + + # Generate internal metrics documentation + if internal_output_file: + with open(internal_output_file, 'w') as f: + f.write("= Redpanda Internal Metrics Reference\n") + f.write(":description: Reference documentation for Redpanda internal metrics extracted from source code.\n") + f.write(":page-categories: Management, Monitoring\n") + f.write("\n") + f.write("This document lists all internal metrics found in the Redpanda source code.\n") + f.write("These metrics are primarily for debugging and operational use and may have high cardinality.\n") + f.write("\n") - f.write(f"*Type*: {metric_info.get('type', 'unknown')}\n\n") + # Sort internal metrics by key + sorted_internal = sorted(internal_metrics.items()) - if metric_info.get('labels'): - f.write("*Labels*:\n\n") - for label in sorted(metric_info['labels']): - f.write(f"- `{label}`\n") - f.write("\n") + for metric_key, metric_info in sorted_internal: + # Use full_name as section header, fallback to metric_key if full_name is not available + section_name = metric_info.get('full_name', metric_key) + f.write(f"=== {section_name}\n\n") + + if metric_info.get('description'): + f.write(f"{metric_info['description']}\n\n") + else: + f.write("No description available.\n\n") + + f.write(f"*Type*: {metric_info.get('type', 'unknown')}\n\n") + + if metric_info.get('labels'): + f.write("*Labels*:\n\n") + for label in sorted(metric_info['labels']): + f.write(f"- `{label}`\n") + f.write("\n") + + if metric_info.get('files') and metric_info['files']: + f.write(f"*Source*: `{metric_info['files'][0].get('file', '')}`\n\n") + + f.write("---\n\n") + + # Generate external metrics documentation + if external_output_file: + with open(external_output_file, 'w') as f: + f.write("= Redpanda Public Metrics Reference\n") + f.write(":description: Reference documentation for Redpanda public metrics extracted from source code.\n") + f.write(":page-categories: Management, Monitoring\n") + f.write("\n") + f.write("This document lists all public metrics found in the Redpanda source code.\n") + f.write("These metrics are designed for customer consumption and have low cardinality.\n") + f.write("\n") - if metric_info.get('files') and metric_info['files']: - f.write(f"*Source*: `{metric_info['files'][0].get('file', '')}`\n\n") + # Sort external metrics by key + sorted_external = sorted(external_metrics.items()) - f.write("---\n\n") + for metric_key, metric_info in sorted_external: + # Use full_name as section header, fallback to metric_key if full_name is not available + section_name = metric_info.get('full_name', metric_key) + f.write(f"=== {section_name}\n\n") + + if metric_info.get('description'): + f.write(f"{metric_info['description']}\n\n") + else: + f.write("No description available.\n\n") + + f.write(f"*Type*: {metric_info.get('type', 'unknown')}\n\n") + + if metric_info.get('labels'): + f.write("*Labels*:\n\n") + for label in sorted(metric_info['labels']): + f.write(f"- `{label}`\n") + f.write("\n") + + if metric_info.get('files') and metric_info['files']: + f.write(f"*Source*: `{metric_info['files'][0].get('file', '')}`\n\n") + + f.write("---\n\n") def main(): @@ -173,9 +234,17 @@ def main(): cpp_files, treesitter_parser, cpp_language, args.filter_namespace ) - # Show clean summary - total_metrics = len(metrics_bag.get_all_metrics()) + # Show clean summary with internal/external breakdown + all_metrics = metrics_bag.get_all_metrics() + total_metrics = len(all_metrics) + + # Count internal vs external metrics + internal_count = sum(1 for metric in all_metrics.values() if metric.get('metric_type') == 'internal') + external_count = sum(1 for metric in all_metrics.values() if metric.get('metric_type') == 'external') + print(f"βœ… Successfully extracted {total_metrics} metrics from {len(cpp_files)} C++ files") + print(f"Internal metrics: {internal_count}") + print(f"External metrics: {external_count}") # Output JSON if args.verbose: @@ -184,14 +253,28 @@ def main(): json.dump(metrics_bag.to_dict(), f, indent=2) # Output AsciiDoc if requested + if args.internal_asciidoc or args.external_asciidoc: + if args.verbose: + if args.internal_asciidoc: + logger.info(f"Writing internal metrics AsciiDoc output to {args.internal_asciidoc}") + if args.external_asciidoc: + logger.info(f"Writing external metrics AsciiDoc output to {args.external_asciidoc}") + generate_asciidoc_by_type(metrics_bag, args.internal_asciidoc, args.external_asciidoc) + + # Handle legacy --asciidoc argument (generate both files) if args.asciidoc: if args.verbose: - logger.info(f"Writing AsciiDoc output to {args.asciidoc}") - generate_asciidoc(metrics_bag, args.asciidoc) + logger.info(f"Writing legacy AsciiDoc output to {args.asciidoc}") + # For backward compatibility, generate both internal and external in one file + generate_asciidoc_by_type(metrics_bag, args.asciidoc, None) print(f"πŸ“„ Output written to: {args.output}") + if args.internal_asciidoc: + print(f"πŸ“„ Internal metrics AsciiDoc written to: {args.internal_asciidoc}") + if args.external_asciidoc: + print(f"πŸ“„ External metrics AsciiDoc written to: {args.external_asciidoc}") if args.asciidoc: - print(f"πŸ“„ AsciiDoc written to: {args.asciidoc}") + print(f"πŸ“„ Legacy AsciiDoc written to: {args.asciidoc}") # Show breakdown by type metrics_by_type = {} From 52d8d86855b94892e58c14d0350cebd9816fdd47 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 22 Jul 2025 16:44:18 -0300 Subject: [PATCH 13/21] grouping and doc intro --- tools/metrics-extractor/metrics_extractor.py | 217 ++++++++++++++----- 1 file changed, 163 insertions(+), 54 deletions(-) diff --git a/tools/metrics-extractor/metrics_extractor.py b/tools/metrics-extractor/metrics_extractor.py index a3574ab..60a940a 100644 --- a/tools/metrics-extractor/metrics_extractor.py +++ b/tools/metrics-extractor/metrics_extractor.py @@ -120,79 +120,188 @@ def generate_asciidoc_by_type(metrics_bag, internal_output_file, external_output else: external_metrics[metric_key] = metric_info + # Group metrics by category/prefix for better organization + def group_metrics_by_category(metrics_dict): + """Group metrics by their prefix (first part before underscore)""" + groups = {} + for metric_key, metric_info in metrics_dict.items(): + # Extract category from full_name or fallback to metric_key + full_name = metric_info.get('full_name', metric_key) + + # Remove redpanda_ or vectorized_ prefix first + clean_name = full_name + if clean_name.startswith('redpanda_'): + clean_name = clean_name[9:] # Remove 'redpanda_' + elif clean_name.startswith('vectorized_'): + clean_name = clean_name[11:] # Remove 'vectorized_' + + # Get the category (first part before underscore) + parts = clean_name.split('_') + category = parts[0] if parts else 'other' + + # Create more meaningful category names + category_mapping = { + 'cluster': 'Cluster metrics', + 'kafka': 'Kafka metrics', + 'raft': 'Raft metrics', + 'storage': 'Storage metrics', + 'memory': 'Memory metrics', + 'io': 'I/O metrics', + 'rpc': 'RPC metrics', + 'cloud': 'Cloud storage metrics', + 'application': 'Application metrics', + 'reactor': 'Reactor metrics', + 'scheduler': 'Scheduler metrics', + 'network': 'Network metrics', + 'internal': 'Internal RPC metrics', + 'pandaproxy': 'REST proxy metrics', + 'rest': 'REST proxy metrics', + 'schema': 'Schema registry metrics', + 'transform': 'Data transforms metrics', + 'wasm': 'Data transforms metrics', + 'security': 'Security metrics', + 'authorization': 'Security metrics', + 'tls': 'TLS metrics', + 'debug': 'Debug bundle metrics', + 'alien': 'Cross-shard metrics', + 'archival': 'Archival metrics', + 'ntp': 'Partition metrics', + 'space': 'Space management metrics', + 'chunk': 'Chunk cache metrics', + 'tx': 'Transaction metrics', + 'leader': 'Leader balancer metrics', + 'node': 'Node status metrics', + 'stall': 'Stall detector metrics', + 'httpd': 'HTTP server metrics', + 'host': 'Host metrics', + 'uptime': 'Infrastructure metrics', + 'cpu': 'Infrastructure metrics', + 'iceberg': 'Iceberg metrics' + } + + category_name = category_mapping.get(category, f'{category.title()} metrics') + + if category_name not in groups: + groups[category_name] = {} + groups[category_name][metric_key] = metric_info + + return groups + # Generate internal metrics documentation if internal_output_file: with open(internal_output_file, 'w') as f: - f.write("= Redpanda Internal Metrics Reference\n") - f.write(":description: Reference documentation for Redpanda internal metrics extracted from source code.\n") - f.write(":page-categories: Management, Monitoring\n") + f.write("= Internal Metrics\n") + f.write(":description: Redpanda internal metrics for detailed analysis, debugging, and troubleshooting.\n") + f.write(":page-aliases: reference:internal-metrics.adoc\n") + f.write("\n") + f.write("This section provides reference descriptions about the internal metrics exported from Redpanda's `/metrics` endpoint.\n") + f.write("\n") + f.write("include::shared:partial$metrics-usage-tip.adoc[]\n") + f.write("\n") + f.write("[IMPORTANT]\n") + f.write("====\n") + f.write("In a live system, Redpanda metrics are exported only for features that are in use. For example, a metric for consumer groups is not exported when no groups are registered.\n") + f.write("\n") + f.write("To see the available internal metrics in your system, query the `/metrics` endpoint:\n") + f.write("\n") + f.write("[,bash]\n") + f.write("----\n") + f.write("curl http://:9644/metrics | grep \"[HELP|TYPE]\"\n") + f.write("----\n") + f.write("====\n") + f.write("\n") + f.write("Internal metrics (`/metrics`) can generate thousands of metric series in production environments. Use them judiciously in monitoring systems to avoid performance issues. For alerting and dashboards, prefer public metrics (`/public_metrics`) which are optimized for lower cardinality.\n") f.write("\n") - f.write("This document lists all internal metrics found in the Redpanda source code.\n") - f.write("These metrics are primarily for debugging and operational use and may have high cardinality.\n") + f.write("The xref:reference:properties/cluster-properties.adoc#aggregate_metrics[aggregate_metrics] cluster property controls internal metrics cardinality. When you enable this property, internal metrics combine labels (like shard) to reduce the number of series. Public metrics always combine labels, regardless of this setting.\n") f.write("\n") - # Sort internal metrics by key - sorted_internal = sorted(internal_metrics.items()) + # Group and sort internal metrics + internal_groups = group_metrics_by_category(internal_metrics) - for metric_key, metric_info in sorted_internal: - # Use full_name as section header, fallback to metric_key if full_name is not available - section_name = metric_info.get('full_name', metric_key) - f.write(f"=== {section_name}\n\n") + for group_name in sorted(internal_groups.keys()): + f.write(f"== {group_name}\n\n") - if metric_info.get('description'): - f.write(f"{metric_info['description']}\n\n") - else: - f.write("No description available.\n\n") + # Sort metrics within each group + sorted_group_metrics = sorted(internal_groups[group_name].items()) - f.write(f"*Type*: {metric_info.get('type', 'unknown')}\n\n") - - if metric_info.get('labels'): - f.write("*Labels*:\n\n") - for label in sorted(metric_info['labels']): - f.write(f"- `{label}`\n") - f.write("\n") - - if metric_info.get('files') and metric_info['files']: - f.write(f"*Source*: `{metric_info['files'][0].get('file', '')}`\n\n") - - f.write("---\n\n") + for metric_key, metric_info in sorted_group_metrics: + # Use full_name as section header, fallback to metric_key if full_name is not available + section_name = metric_info.get('full_name', metric_key) + f.write(f"=== {section_name}\n\n") + + if metric_info.get('description'): + f.write(f"{metric_info['description']}\n\n") + else: + f.write("No description available.\n\n") + + f.write(f"*Type*: {metric_info.get('type', 'unknown')}\n\n") + + if metric_info.get('labels'): + f.write("*Labels*:\n\n") + for label in sorted(metric_info['labels']): + f.write(f"- `{label}`\n") + f.write("\n") + + f.write("---\n\n") # Generate external metrics documentation if external_output_file: with open(external_output_file, 'w') as f: - f.write("= Redpanda Public Metrics Reference\n") - f.write(":description: Reference documentation for Redpanda public metrics extracted from source code.\n") - f.write(":page-categories: Management, Monitoring\n") + f.write("= Public Metrics\n") + f.write(":description: Public metrics to create your system dashboard.\n") + f.write("// tag::single-source[]\n") + f.write("\n") + f.write("This section provides reference descriptions for the public metrics exported from Redpanda's `/public_metrics` endpoint.\n") + f.write("\n") + f.write("// Cloud does not expose the internal metrics.\n") + f.write("ifndef::env-cloud[]\n") + f.write("include::shared:partial$metrics-usage-tip.adoc[]\n") + f.write("endif::[]\n") + f.write("\n") + f.write("[IMPORTANT]\n") + f.write("====\n") + f.write("In a live system, Redpanda metrics are exported only for features that are in use. For example, Redpanda does not export metrics for consumer groups if no groups are registered.\n") f.write("\n") - f.write("This document lists all public metrics found in the Redpanda source code.\n") - f.write("These metrics are designed for customer consumption and have low cardinality.\n") + f.write("To see the available public metrics in your system, query the `/public_metrics` endpoint:\n") + f.write("\n") + f.write("[,bash]\n") + f.write("----\n") + f.write("curl http://:9644/public_metrics | grep \"[HELP|TYPE]\"\n") + f.write("----\n") + f.write("\n") + f.write("====\n") f.write("\n") - # Sort external metrics by key - sorted_external = sorted(external_metrics.items()) + # Group and sort external metrics + external_groups = group_metrics_by_category(external_metrics) - for metric_key, metric_info in sorted_external: - # Use full_name as section header, fallback to metric_key if full_name is not available - section_name = metric_info.get('full_name', metric_key) - f.write(f"=== {section_name}\n\n") - - if metric_info.get('description'): - f.write(f"{metric_info['description']}\n\n") - else: - f.write("No description available.\n\n") + for group_name in sorted(external_groups.keys()): + f.write(f"== {group_name}\n\n") - f.write(f"*Type*: {metric_info.get('type', 'unknown')}\n\n") + # Sort metrics within each group + sorted_group_metrics = sorted(external_groups[group_name].items()) - if metric_info.get('labels'): - f.write("*Labels*:\n\n") - for label in sorted(metric_info['labels']): - f.write(f"- `{label}`\n") - f.write("\n") - - if metric_info.get('files') and metric_info['files']: - f.write(f"*Source*: `{metric_info['files'][0].get('file', '')}`\n\n") - - f.write("---\n\n") + for metric_key, metric_info in sorted_group_metrics: + # Use full_name as section header, fallback to metric_key if full_name is not available + section_name = metric_info.get('full_name', metric_key) + f.write(f"=== {section_name}\n\n") + + if metric_info.get('description'): + f.write(f"{metric_info['description']}\n\n") + else: + f.write("No description available.\n\n") + + f.write(f"*Type*: {metric_info.get('type', 'unknown')}\n\n") + + if metric_info.get('labels'): + f.write("*Labels*:\n\n") + for label in sorted(metric_info['labels']): + f.write(f"- `{label}`\n") + f.write("\n") + + f.write("---\n\n") + + f.write("// end::single-source[]\n") def main(): From 464660c201dcdf20b2df503faba0ce64065ffcf7 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 22 Jul 2025 18:01:09 -0300 Subject: [PATCH 14/21] fix full description --- tools/metrics-extractor/metrics_parser.py | 83 +++++++++++++++++++---- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py index 6b8de7b..95a2e5d 100644 --- a/tools/metrics-extractor/metrics_parser.py +++ b/tools/metrics-extractor/metrics_parser.py @@ -321,31 +321,84 @@ def extract_metric_details(args_node, source_code): metric_name = "" description = "" + # Find all string literals and their positions string_literals = [] - def collect_strings(node): - # Recursively find all string literals within the arguments + def collect_string_info(node): + """Recursively find all string literals with their positions""" if node.type == "string_literal": text = node.text.decode("utf-8", errors="ignore") - string_literals.append(unquote_string(text)) + unquoted = unquote_string(text) + start_pos = node.start_point + end_pos = node.end_point + string_literals.append({ + 'text': unquoted, + 'start': start_pos, + 'end': end_pos, + 'raw': text + }) for child in node.children: - collect_strings(child) + collect_string_info(child) - collect_strings(args_node) + collect_string_info(args_node) + + # Sort string literals by their position in the source + string_literals.sort(key=lambda x: (x['start'][0], x['start'][1])) # First string literal is the metric name if string_literals: - metric_name = string_literals[0] + metric_name = string_literals[0]['text'] + + # Look for description by finding sm::description() calls or consecutive string literals + args_text = args_node.text.decode("utf-8", errors="ignore") - # The second string literal is usually the description. - # This is a simplification; a more complex heuristic could be needed if - # the description isn't the second string. - if len(string_literals) > 1: - # A simple check to see if the argument list contains "description" - # to be more certain. - args_text = args_node.text.decode("utf-8", errors="ignore") - if "description" in args_text: - description = string_literals[1] + if "description" in args_text: + # Find the description call and extract concatenated strings + description_strings = [] + in_description = False + + for i, str_info in enumerate(string_literals): + # Skip the first string which is the metric name + if i == 0: + continue + + # Check if this string is likely part of a description + # by looking at the surrounding code context + start_byte = str_info['start'][0] * 100 + str_info['start'][1] # rough byte estimate + + # Get some context around this string + node_start = args_node.start_byte + node_end = args_node.end_byte + + # Extract context to see if this is in a description call + context_start = max(node_start, start_byte - 100) + context_end = min(node_end, start_byte + len(str_info['raw']) + 100) + context = source_code[context_start:context_end].decode("utf-8", errors="ignore") + + # If we find "description" before this string, start collecting + if "description" in context and not in_description: + in_description = True + description_strings.append(str_info['text']) + elif in_description: + # Check if this string is adjacent to the previous one (likely concatenated) + if i > 1: + prev_str = string_literals[i-1] + # If strings are on consecutive lines or close together, they're likely concatenated + line_diff = str_info['start'][0] - prev_str['end'][0] + if line_diff <= 1: # Same line or next line + description_strings.append(str_info['text']) + else: + break # No longer part of the same description + else: + description_strings.append(str_info['text']) + + # Concatenate all description strings + if description_strings: + description = ''.join(description_strings) + elif len(string_literals) > 1: + # Fallback: if we found "description" but couldn't parse it properly, + # just use the second string literal + description = string_literals[1]['text'] return metric_name, description From 6fcd9478af976979acc86de9e0ea11d9e234c64e Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 22 Jul 2025 18:19:20 -0300 Subject: [PATCH 15/21] fix description extraction --- tools/metrics-extractor/metrics_extractor.py | 66 ++++++------ tools/metrics-extractor/metrics_parser.py | 105 +++++++++++-------- 2 files changed, 100 insertions(+), 71 deletions(-) diff --git a/tools/metrics-extractor/metrics_extractor.py b/tools/metrics-extractor/metrics_extractor.py index 60a940a..49f46c2 100644 --- a/tools/metrics-extractor/metrics_extractor.py +++ b/tools/metrics-extractor/metrics_extractor.py @@ -145,15 +145,15 @@ def group_metrics_by_category(metrics_dict): 'kafka': 'Kafka metrics', 'raft': 'Raft metrics', 'storage': 'Storage metrics', - 'memory': 'Memory metrics', - 'io': 'I/O metrics', + 'memory': 'Infrastructure metrics', + 'io': 'Infrastructure metrics', 'rpc': 'RPC metrics', 'cloud': 'Cloud storage metrics', 'application': 'Application metrics', - 'reactor': 'Reactor metrics', - 'scheduler': 'Scheduler metrics', - 'network': 'Network metrics', - 'internal': 'Internal RPC metrics', + 'reactor': 'Infrastructure metrics', + 'scheduler': 'Infrastructure metrics', + 'network': 'Infrastructure metrics', + 'internal': 'RPC metrics', 'pandaproxy': 'REST proxy metrics', 'rest': 'REST proxy metrics', 'schema': 'Schema registry metrics', @@ -161,25 +161,38 @@ def group_metrics_by_category(metrics_dict): 'wasm': 'Data transforms metrics', 'security': 'Security metrics', 'authorization': 'Security metrics', - 'tls': 'TLS metrics', + 'tls': 'Security metrics', 'debug': 'Debug bundle metrics', - 'alien': 'Cross-shard metrics', - 'archival': 'Archival metrics', + 'alien': 'Infrastructure metrics', + 'archival': 'Cloud storage metrics', 'ntp': 'Partition metrics', - 'space': 'Space management metrics', - 'chunk': 'Chunk cache metrics', + 'space': 'Storage metrics', + 'chunk': 'Storage metrics', 'tx': 'Transaction metrics', - 'leader': 'Leader balancer metrics', - 'node': 'Node status metrics', - 'stall': 'Stall detector metrics', - 'httpd': 'HTTP server metrics', - 'host': 'Host metrics', + 'leader': 'Raft metrics', + 'node': 'Raft metrics', + 'stall': 'Infrastructure metrics', + 'httpd': 'Infrastructure metrics', + 'host': 'Infrastructure metrics', 'uptime': 'Infrastructure metrics', 'cpu': 'Infrastructure metrics', 'iceberg': 'Iceberg metrics' } - category_name = category_mapping.get(category, f'{category.title()} metrics') + # Use the mapping, but fallback to a few broad categories instead of creating many + category_name = category_mapping.get(category) + if not category_name: + # Group unmapped categories into broader buckets + if category in ['active', 'adjacent', 'anomalies', 'available', 'backlog', 'batch', 'batches', 'brokers', 'buffer', 'bytes', 'cached', 'certificate', 'chunked', 'cleanly', 'client', 'closed', 'committed', 'compacted', 'compaction', 'complete', 'connection', 'connections', 'connects', 'consumed', 'corrupted']: + category_name = 'Application metrics' + elif category in ['data', 'datalake', 'decompressed', 'dirty', 'disk', 'dispatch', 'dlq', 'end', 'error', 'errors', 'events', 'failed', 'failures', 'fetch', 'files', 'high', 'housekeeping']: + category_name = 'Application metrics' + elif category in ['in', 'inflight', 'invalid', 'lag', 'last', 'latest', 'loaded', 'local', 'log', 'logs', 'max', 'method', 'non', 'num', 'offsets', 'out', 'parquet', 'partition', 'partitions']: + category_name = 'Application metrics' + elif category in ['queued', 'raw', 'read', 'received', 'reclaim', 'records', 'request', 'requests', 'result', 'retention', 'segments', 'sent', 'server', 'service', 'shares', 'start', 'state', 'successful', 'target', 'throttle', 'tombstones', 'topics', 'total', 'traffic', 'translations', 'trust', 'truststore', 'unavailable', 'under', 'urgent', 'write', 'written']: + category_name = 'Application metrics' + else: + category_name = 'Other metrics' if category_name not in groups: groups[category_name] = {} @@ -351,23 +364,15 @@ def main(): internal_count = sum(1 for metric in all_metrics.values() if metric.get('metric_type') == 'internal') external_count = sum(1 for metric in all_metrics.values() if metric.get('metric_type') == 'external') - print(f"βœ… Successfully extracted {total_metrics} metrics from {len(cpp_files)} C++ files") + print(f"βœ… Successfully extracted {total_metrics} metrics from {len(cpp_files)} C++ files.") print(f"Internal metrics: {internal_count}") print(f"External metrics: {external_count}") - # Output JSON - if args.verbose: - logger.info(f"Writing JSON output to {args.output}") with open(args.output, 'w') as f: json.dump(metrics_bag.to_dict(), f, indent=2) # Output AsciiDoc if requested if args.internal_asciidoc or args.external_asciidoc: - if args.verbose: - if args.internal_asciidoc: - logger.info(f"Writing internal metrics AsciiDoc output to {args.internal_asciidoc}") - if args.external_asciidoc: - logger.info(f"Writing external metrics AsciiDoc output to {args.external_asciidoc}") generate_asciidoc_by_type(metrics_bag, args.internal_asciidoc, args.external_asciidoc) # Handle legacy --asciidoc argument (generate both files) @@ -377,13 +382,14 @@ def main(): # For backward compatibility, generate both internal and external in one file generate_asciidoc_by_type(metrics_bag, args.asciidoc, None) - print(f"πŸ“„ Output written to: {args.output}") + # Only show summary messages, not duplicate file outputs + print(f"πŸ“„ JSON output: {args.output}") if args.internal_asciidoc: - print(f"πŸ“„ Internal metrics AsciiDoc written to: {args.internal_asciidoc}") + print(f"πŸ“„ Internal metrics: {args.internal_asciidoc}") if args.external_asciidoc: - print(f"πŸ“„ External metrics AsciiDoc written to: {args.external_asciidoc}") + print(f"πŸ“„ External metrics: {args.external_asciidoc}") if args.asciidoc: - print(f"πŸ“„ Legacy AsciiDoc written to: {args.asciidoc}") + print(f"πŸ“„ Legacy AsciiDoc: {args.asciidoc}") # Show breakdown by type metrics_by_type = {} diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py index 95a2e5d..111f3bc 100644 --- a/tools/metrics-extractor/metrics_parser.py +++ b/tools/metrics-extractor/metrics_parser.py @@ -353,52 +353,75 @@ def collect_string_info(node): args_text = args_node.text.decode("utf-8", errors="ignore") if "description" in args_text: - # Find the description call and extract concatenated strings - description_strings = [] - in_description = False + # Use a more robust approach to find description strings + # Look for the pattern: sm::description(...) and extract all strings within it + description_pattern = r'sm::description\s*\(\s*([^)]*)\)' + desc_match = re.search(description_pattern, args_text, re.DOTALL) - for i, str_info in enumerate(string_literals): - # Skip the first string which is the metric name - if i == 0: - continue - - # Check if this string is likely part of a description - # by looking at the surrounding code context - start_byte = str_info['start'][0] * 100 + str_info['start'][1] # rough byte estimate + if desc_match: + desc_content = desc_match.group(1) + # Extract all string literals from the description content + # Handle both quoted strings and raw strings + desc_strings = [] - # Get some context around this string - node_start = args_node.start_byte - node_end = args_node.end_byte + # Find all string literals in the description content + string_pattern = r'(?:R"[^(]*\(([^)]*)\)[^"]*"|"([^"\\]*(\\.[^"\\]*)*)")' + for match in re.finditer(string_pattern, desc_content, re.DOTALL): + if match.group(1): # Raw string + desc_strings.append(match.group(1)) + elif match.group(2): # Regular string + # Handle escape sequences + content = match.group(2) + content = content.replace('\\"', '"') + content = content.replace('\\\\', '\\') + content = content.replace('\\n', '\n') + content = content.replace('\\t', '\t') + desc_strings.append(content) - # Extract context to see if this is in a description call - context_start = max(node_start, start_byte - 100) - context_end = min(node_end, start_byte + len(str_info['raw']) + 100) - context = source_code[context_start:context_end].decode("utf-8", errors="ignore") + if desc_strings: + description = ''.join(desc_strings) + + # Fallback: if regex approach didn't work, try the AST approach + if not description: + # Find strings that come after we see "description" in the context + description_strings = [] + found_description = False - # If we find "description" before this string, start collecting - if "description" in context and not in_description: - in_description = True - description_strings.append(str_info['text']) - elif in_description: - # Check if this string is adjacent to the previous one (likely concatenated) - if i > 1: - prev_str = string_literals[i-1] - # If strings are on consecutive lines or close together, they're likely concatenated - line_diff = str_info['start'][0] - prev_str['end'][0] - if line_diff <= 1: # Same line or next line + for i, str_info in enumerate(string_literals): + # Skip the first string which is the metric name + if i == 0: + continue + + # Get the full args context and find position of this string + str_pos = args_text.find(str_info['raw']) + if str_pos != -1: + context_before = args_text[:str_pos] + if "description" in context_before and not found_description: + found_description = True description_strings.append(str_info['text']) - else: - break # No longer part of the same description - else: - description_strings.append(str_info['text']) - - # Concatenate all description strings - if description_strings: - description = ''.join(description_strings) - elif len(string_literals) > 1: - # Fallback: if we found "description" but couldn't parse it properly, - # just use the second string literal - description = string_literals[1]['text'] + elif found_description: + # Check if this string is adjacent (C++ string concatenation) + # Look at the raw text between this and previous string + if i > 1: + prev_str = string_literals[i-1] + prev_pos = args_text.find(prev_str['raw']) + if prev_pos != -1: + between_text = args_text[prev_pos + len(prev_str['raw']):str_pos].strip() + # If there's only whitespace/newlines between strings, they're concatenated + if not between_text or all(c in ' \t\n\r' for c in between_text): + description_strings.append(str_info['text']) + else: + break # Found something else, stop collecting + else: + break + else: + description_strings.append(str_info['text']) + + if description_strings: + description = ''.join(description_strings) + elif len(string_literals) > 1: + # Final fallback: just use the second string literal + description = string_literals[1]['text'] return metric_name, description From c2fad9722b1ca0e592ddc9b87e4f80421563acbf Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 22 Jul 2025 19:00:14 -0300 Subject: [PATCH 16/21] final fixes --- tools/metrics-extractor/metrics_extractor.py | 69 ++++++++++-- tools/metrics-extractor/metrics_parser.py | 109 ++++++++----------- 2 files changed, 107 insertions(+), 71 deletions(-) diff --git a/tools/metrics-extractor/metrics_extractor.py b/tools/metrics-extractor/metrics_extractor.py index 49f46c2..8e88bed 100644 --- a/tools/metrics-extractor/metrics_extractor.py +++ b/tools/metrics-extractor/metrics_extractor.py @@ -105,6 +105,55 @@ def parse_args(): return parser.parse_args() +def clean_description(description): + """Ensure description ends with appropriate punctuation""" + if not description: + return description + + description = description.strip() + if description and not description.endswith(('.', '!', '?')): + description += '.' + + return description + + +def clean_labels(labels): + """Clean up labels by removing whitespace and deduplicating""" + if not labels: + return [] + + cleaned_labels = set() + simple_labels = set() # Track simple labels to avoid adding redundant braced versions + + for label in labels: + # Remove extra whitespace and newlines + clean_label = ' '.join(label.split()) + + # Skip empty labels + if not clean_label: + continue + + # Handle cases like "{shard}" vs "shard" - prefer the simpler form + if clean_label.startswith('{') and clean_label.endswith('}'): + # Extract the content inside braces + inner_content = clean_label[1:-1].strip() + # If it's a simple label (no comma), prefer the unbrace version + if ',' not in inner_content and inner_content: + simple_label = inner_content.strip() + simple_labels.add(simple_label) + cleaned_labels.add(simple_label) # Add the simple version + else: + # Complex label with commas, keep the braced version + cleaned_labels.add(clean_label) + else: + # Simple label + simple_labels.add(clean_label) + cleaned_labels.add(clean_label) + + # Convert back to sorted list + return sorted(list(cleaned_labels)) + + def generate_asciidoc_by_type(metrics_bag, internal_output_file, external_output_file): """Generate separate AsciiDoc documentation for internal and external metrics""" all_metrics = metrics_bag.get_all_metrics() @@ -242,16 +291,18 @@ def group_metrics_by_category(metrics_dict): section_name = metric_info.get('full_name', metric_key) f.write(f"=== {section_name}\n\n") - if metric_info.get('description'): - f.write(f"{metric_info['description']}\n\n") + description = clean_description(metric_info.get('description')) + if description: + f.write(f"{description}\n\n") else: f.write("No description available.\n\n") f.write(f"*Type*: {metric_info.get('type', 'unknown')}\n\n") - if metric_info.get('labels'): + cleaned_labels = clean_labels(metric_info.get('labels', [])) + if cleaned_labels: f.write("*Labels*:\n\n") - for label in sorted(metric_info['labels']): + for label in cleaned_labels: f.write(f"- `{label}`\n") f.write("\n") @@ -299,16 +350,18 @@ def group_metrics_by_category(metrics_dict): section_name = metric_info.get('full_name', metric_key) f.write(f"=== {section_name}\n\n") - if metric_info.get('description'): - f.write(f"{metric_info['description']}\n\n") + description = clean_description(metric_info.get('description')) + if description: + f.write(f"{description}\n\n") else: f.write("No description available.\n\n") f.write(f"*Type*: {metric_info.get('type', 'unknown')}\n\n") - if metric_info.get('labels'): + cleaned_labels = clean_labels(metric_info.get('labels', [])) + if cleaned_labels: f.write("*Labels*:\n\n") - for label in sorted(metric_info['labels']): + for label in cleaned_labels: f.write(f"- `{label}`\n") f.write("\n") diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py index 111f3bc..08b98bf 100644 --- a/tools/metrics-extractor/metrics_parser.py +++ b/tools/metrics-extractor/metrics_parser.py @@ -353,75 +353,58 @@ def collect_string_info(node): args_text = args_node.text.decode("utf-8", errors="ignore") if "description" in args_text: - # Use a more robust approach to find description strings - # Look for the pattern: sm::description(...) and extract all strings within it - description_pattern = r'sm::description\s*\(\s*([^)]*)\)' - desc_match = re.search(description_pattern, args_text, re.DOTALL) + # Improved AST-based approach to find all strings in description + description_strings = [] + found_description = False - if desc_match: - desc_content = desc_match.group(1) - # Extract all string literals from the description content - # Handle both quoted strings and raw strings - desc_strings = [] - - # Find all string literals in the description content - string_pattern = r'(?:R"[^(]*\(([^)]*)\)[^"]*"|"([^"\\]*(\\.[^"\\]*)*)")' - for match in re.finditer(string_pattern, desc_content, re.DOTALL): - if match.group(1): # Raw string - desc_strings.append(match.group(1)) - elif match.group(2): # Regular string - # Handle escape sequences - content = match.group(2) - content = content.replace('\\"', '"') - content = content.replace('\\\\', '\\') - content = content.replace('\\n', '\n') - content = content.replace('\\t', '\t') - desc_strings.append(content) - - if desc_strings: - description = ''.join(desc_strings) - - # Fallback: if regex approach didn't work, try the AST approach - if not description: - # Find strings that come after we see "description" in the context - description_strings = [] - found_description = False + for i, str_info in enumerate(string_literals): + # Skip the first string which is the metric name + if i == 0: + continue - for i, str_info in enumerate(string_literals): - # Skip the first string which is the metric name - if i == 0: - continue + # Get the full args context and find position of this string + str_pos = args_text.find(str_info['raw']) + if str_pos != -1: + context_before = args_text[:str_pos] - # Get the full args context and find position of this string - str_pos = args_text.find(str_info['raw']) - if str_pos != -1: - context_before = args_text[:str_pos] - if "description" in context_before and not found_description: - found_description = True - description_strings.append(str_info['text']) - elif found_description: - # Check if this string is adjacent (C++ string concatenation) - # Look at the raw text between this and previous string - if i > 1: - prev_str = string_literals[i-1] - prev_pos = args_text.find(prev_str['raw']) - if prev_pos != -1: - between_text = args_text[prev_pos + len(prev_str['raw']):str_pos].strip() - # If there's only whitespace/newlines between strings, they're concatenated - if not between_text or all(c in ' \t\n\r' for c in between_text): - description_strings.append(str_info['text']) - else: - break # Found something else, stop collecting + # Check if this string comes after "description" in the context + if "description" in context_before and not found_description: + found_description = True + description_strings.append(str_info['text']) + + # Look ahead to collect all consecutive string literals + # that are part of the same description (C++ auto-concatenation) + for j in range(i + 1, len(string_literals)): + next_str = string_literals[j] + next_pos = args_text.find(next_str['raw']) + + if next_pos != -1: + # Check if there's only whitespace/comments between strings + between_text = args_text[str_pos + len(str_info['raw']):next_pos] + + # Clean up the between text - remove comments and normalize whitespace + between_clean = re.sub(r'//.*?$', '', between_text, flags=re.MULTILINE) + between_clean = re.sub(r'/\*.*?\*/', '', between_clean, flags=re.DOTALL) + between_clean = between_clean.strip() + + # If only whitespace/punctuation between strings, they're concatenated + if not between_clean or all(c in ' \t\n\r,)' for c in between_clean): + description_strings.append(next_str['text']) + str_info = next_str # Update position for next iteration + str_pos = next_pos else: + # Found something else, stop collecting break else: - description_strings.append(str_info['text']) - - if description_strings: - description = ''.join(description_strings) - elif len(string_literals) > 1: - # Final fallback: just use the second string literal - description = string_literals[1]['text'] + break + break + + # Join all collected description strings + if description_strings: + description = ''.join(description_strings) + elif len(string_literals) > 1: + # Final fallback: just use the second string literal + description = string_literals[1]['text'] return metric_name, description From a12dd12d9b4c3dd2ee59497a6caf03526081d170 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 22 Jul 2025 19:37:13 -0300 Subject: [PATCH 17/21] add docs and fix github tagging --- README.adoc | 40 + bin/doc-tools.js | 50 +- cli-utils/generate-cluster-docs.sh | 8 +- package.json | 4 +- tools/metrics-extractor/Makefile | 18 +- tools/metrics-extractor/README.adoc | 357 +- tools/metrics-extractor/example.py | 202 - tools/metrics-extractor/metrics.json | 5036 ------------------ tools/metrics-extractor/metrics_extractor.py | 19 +- tools/metrics-extractor/sample.json | 251 - tools/metrics-extractor/test_filtered.json | 10 - 11 files changed, 159 insertions(+), 5836 deletions(-) delete mode 100644 tools/metrics-extractor/example.py delete mode 100644 tools/metrics-extractor/metrics.json delete mode 100644 tools/metrics-extractor/sample.json delete mode 100644 tools/metrics-extractor/test_filtered.json diff --git a/README.adoc b/README.adoc index 2a9e4f1..5097509 100644 --- a/README.adoc +++ b/README.adoc @@ -966,6 +966,46 @@ asciidoc: - '@redpanda-data/docs-extensions-and-macros/macros/rp-connect-components' ``` +== CLI Tools + +This library provides automated documentation generation tools for writers working with Redpanda documentation. + +=== Metrics Documentation + +Generate metrics reference documentation for Redpanda: + +[,bash] +---- +# Extract from a specific GitHub tag/branch (recommended) +npx doc-tools generate source-metrics-docs --tag v25.2.1-rc4 + +# Extract from a local repository +npx doc-tools generate metrics-docs --redpanda-repo /path/to/redpanda + +# Legacy Docker-based extraction +npx doc-tools generate metrics-docs-legacy --tag v25.2.1-rc4 +---- + +All commands generate separate files for internal and external metrics: + +* `autogenerated/internal_metrics_reference.adoc` - Internal metrics for engineering teams +* `autogenerated/public_metrics_reference.adoc` - Public metrics for documentation +* `autogenerated/metrics.json` - Machine-readable metrics data + +=== Other Documentation Tools + +* `property-docs`: Generate configuration property documentation +* `rpk-docs`: Generate RPK CLI documentation +* `helm-spec`: Generate Helm chart specifications +* `crd-spec`: Generate CRD specifications + +For complete command options, run: + +[,bash] +---- +npx doc-tools generate --help +---- + == Development quickstart This section provides information on how to develop this project. diff --git a/bin/doc-tools.js b/bin/doc-tools.js index aad85eb..472c522 100755 --- a/bin/doc-tools.js +++ b/bin/doc-tools.js @@ -460,7 +460,51 @@ function diffDirs(kind, oldTag, newTag) { automation .command('metrics-docs') - .description('Generate JSON and AsciiDoc documentation for Redpanda metrics') + .description('Generate JSON and AsciiDoc documentation for Redpanda metrics from source code') + .requiredOption('-r, --redpanda-repo ', 'Path to the Redpanda repository root directory') + .option('--json-output ', 'Custom path for JSON output file', 'autogenerated/metrics.json') + .option('--internal-asciidoc ', 'Custom path for internal metrics AsciiDoc file', 'autogenerated/internal_metrics_reference.adoc') + .option('--external-asciidoc ', 'Custom path for external/public metrics AsciiDoc file', 'autogenerated/public_metrics_reference.adoc') + .action((options) => { + console.log(`🎯 Starting enhanced metrics extraction from source code`); + + verifyMetricsExtractorDependencies(); + + // Verify Redpanda repository path exists + if (!fs.existsSync(options.redpandaRepo)) { + console.error(`❌ Redpanda repository path does not exist: ${options.redpandaRepo}`); + process.exit(1); + } + + console.log(`⏳ Extracting metrics from ${options.redpandaRepo}...`); + + const startTime = Date.now(); + const result = spawnSync('python3', [ + path.join(__dirname, '../tools/metrics-extractor/metrics_extractor.py'), + '--redpanda-repo', options.redpandaRepo, + '--json-output', options.jsonOutput, + '--internal-asciidoc', options.internalAsciidoc, + '--external-asciidoc', options.externalAsciidoc + ], { stdio: 'inherit' }); + + const duration = ((Date.now() - startTime) / 1000).toFixed(1); + + if (result.status !== 0) { + console.error(`❌ Metrics extraction failed with exit code ${result.status}`); + process.exit(result.status); + } + + console.log(`βœ… Enhanced metrics extraction completed! (${duration}s)`); + console.log(`πŸ“„ Generated files:`); + console.log(` JSON: ${options.jsonOutput}`); + console.log(` Internal metrics: ${options.internalAsciidoc}`); + console.log(` Public metrics: ${options.externalAsciidoc}`); + process.exit(0); + }); + +automation + .command('metrics-docs-legacy') + .description('Generate JSON and AsciiDoc documentation for Redpanda metrics using Docker cluster (legacy)') .requiredOption('-t, --tag ', 'Redpanda version to use when starting Redpanda in Docker') .option( '--docker-repo ', @@ -479,7 +523,7 @@ automation ) .option('--diff ', 'Also diff autogenerated metrics from β†’ ') .action((options) => { - console.log(`🎯 Starting metrics docs generation for ${options.tag}`); + console.log(`🎯 Starting legacy metrics docs generation for ${options.tag}`); verifyMetricsDependencies(); @@ -502,7 +546,7 @@ automation diffDirs('metrics', oldTag, newTag); } - console.log(`βœ… Metrics docs generation completed!`); + console.log(`βœ… Legacy metrics docs generation completed!`); process.exit(0); }); diff --git a/cli-utils/generate-cluster-docs.sh b/cli-utils/generate-cluster-docs.sh index b5e193a..1e53997 100755 --- a/cli-utils/generate-cluster-docs.sh +++ b/cli-utils/generate-cluster-docs.sh @@ -100,7 +100,7 @@ fi log_step "🐍 Setting up Python environment..." "$SCRIPT_DIR/python-venv.sh" \ "$SCRIPT_DIR/venv" \ - "$SCRIPT_DIR/../tools/metrics/requirements.txt" + "$SCRIPT_DIR/../tools/metrics-extractor/requirements.txt" ############################################################################### # Run documentation generator @@ -108,8 +108,12 @@ log_step "🐍 Setting up Python environment..." log_step "πŸ“ Generating $MODE documentation..." if [[ "$MODE" == "metrics" ]]; then + # Use enhanced metrics extractor with separate internal/external docs "$SCRIPT_DIR/venv/bin/python" \ - "$SCRIPT_DIR/../tools/metrics/metrics.py" "$TAG" + "$SCRIPT_DIR/../tools/metrics-extractor/metrics_extractor.py" \ + --json-output "autogenerated/${TAG}/metrics/metrics.json" \ + --internal-asciidoc "autogenerated/${TAG}/metrics/internal_metrics_reference.adoc" \ + --external-asciidoc "autogenerated/${TAG}/metrics/public_metrics_reference.adoc" else "$SCRIPT_DIR/venv/bin/python" \ "$SCRIPT_DIR/../tools/gen-rpk-ascii.py" "$TAG" diff --git a/package.json b/package.json index ee2a911..04070d1 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "get-console-version": "doc-tools get-console-version", "build": "antora --to-dir docs --fetch local-antora-playbook.yml", "serve": "wds --node-resolve --open preview/test/ --watch --root-dir docs", - "test": "jest" + "test": "jest", + "metrics:extract": "doc-tools metrics-docs", + "metrics:legacy": "doc-tools metrics-docs-legacy" }, "contributors": [ { diff --git a/tools/metrics-extractor/Makefile b/tools/metrics-extractor/Makefile index c1e304b..26f17c8 100644 --- a/tools/metrics-extractor/Makefile +++ b/tools/metrics-extractor/Makefile @@ -47,9 +47,23 @@ clone-redpanda: @mkdir -p tmp @if [ -d "$(REDPANDA_DIR)" ]; then \ echo "Repository already exists, updating..."; \ - cd $(REDPANDA_DIR) && git fetch --all && git checkout $(TAG) && git pull origin $(TAG); \ + cd $(REDPANDA_DIR) && git fetch --all --tags && git checkout $(TAG); \ + if git show-ref --verify --quiet refs/heads/$(TAG); then \ + git pull origin $(TAG); \ + fi; \ else \ - git clone --depth 1 --branch $(TAG) $(REDPANDA_REPO) $(REDPANDA_DIR); \ + echo "Checking if $(TAG) is a branch or tag..."; \ + if git ls-remote --heads $(REDPANDA_REPO) $(TAG) | grep -q $(TAG); then \ + echo "$(TAG) is a branch, cloning..."; \ + git clone --depth 1 --branch $(TAG) $(REDPANDA_REPO) $(REDPANDA_DIR); \ + elif git ls-remote --tags $(REDPANDA_REPO) $(TAG) | grep -q $(TAG); then \ + echo "$(TAG) is a tag, cloning and checking out..."; \ + git clone $(REDPANDA_REPO) $(REDPANDA_DIR); \ + cd $(REDPANDA_DIR) && git checkout $(TAG); \ + else \ + echo "Error: $(TAG) not found as branch or tag in $(REDPANDA_REPO)"; \ + exit 1; \ + fi; \ fi treesitter: diff --git a/tools/metrics-extractor/README.adoc b/tools/metrics-extractor/README.adoc index 1c71b6d..2e1e1fe 100644 --- a/tools/metrics-extractor/README.adoc +++ b/tools/metrics-extractor/README.adoc @@ -1,355 +1,72 @@ = Redpanda Metrics Extractor -:description: Automated extraction of metrics from Redpanda source code using tree-sitter +:description: Automated extraction of metrics from Redpanda source code :page-categories: Development, Documentation, Automation -This tool automatically extracts Redpanda metrics from C++ source code using tree-sitter parsing. It identifies metrics created with various Seastar and Redpanda metric constructors and generates comprehensive documentation. +This tool extracts Redpanda metrics from C++ source code and generates documentation in AsciiDoc format. -== Overview +== Usage for Writers -The metrics extractor uses tree-sitter to parse C++ source code and identify metrics created with these constructors: +=== Quick Start -* `sm::make_gauge` -* `sm::make_counter` -* `sm::make_histogram` -* `sm::make_total_bytes` -* `sm::make_derive` -* `ss::metrics::make_total_operations` -* `ss::metrics::make_current_bytes` - -For each metric found, it extracts: - -* Metric name -* Type (gauge, counter, histogram) -* Description -* Potential labels -* Source file location -* Constructor used - -== Prerequisites - -* Python 3.8 or higher -* Git -* Build tools (gcc/clang for compiling tree-sitter) - -== Installation - -1. Set up the Python virtual environment: -+ -[source,bash] ----- -make setup-venv ----- - -2. Install dependencies: -+ -[source,bash] ----- -make install-deps ----- - -== Usage - -=== Extract Metrics from Redpanda Repository - -Extract metrics from a specific Redpanda version: +Generate metrics documentation from a GitHub tag or branch: [source,bash] ---- -make build TAG=v23.3.1 ----- +# Extract from specific tag (recommended) +npx doc-tools generate source-metrics-docs --tag v25.2.1-rc4 -Extract metrics from the development branch: - -[source,bash] ----- -make build TAG=dev +# Extract from development branch +npx doc-tools generate source-metrics-docs --tag dev ---- -=== Extract Metrics from Local Repository +=== Local Development If you have a local Redpanda repository: [source,bash] ---- -make extract-local REDPANDA_PATH=/path/to/redpanda ----- - -=== Manual Extraction - -Run the extractor directly: - -[source,bash] ----- -python metrics_extractor.py /path/to/redpanda/src \ - --recursive \ - --output metrics.json \ - --asciidoc metrics.adoc \ - --filter-namespace redpanda \ - --verbose +npx doc-tools generate metrics-docs --redpanda-repo /path/to/redpanda ---- -== Output Files +=== Output Files -The extractor generates several output files: +All commands generate three files in the `autogenerated/` directory: -=== JSON Output (`metrics.json`) +* `internal_metrics_reference.adoc` - Internal metrics for engineering documentation +* `public_metrics_reference.adoc` - Public metrics for user-facing documentation +* `metrics.json` - Machine-readable metrics data -Contains structured metric data: +== Technical Details -[source,json] ----- -{ - "metrics": { - "redpanda_kafka_requests_total": { - "name": "redpanda_kafka_requests_total", - "type": "counter", - "description": "Total number of Kafka requests", - "labels": ["request_type", "status"], - "constructor": "make_counter", - "files": [{"file": "src/v/kafka/server/handlers/handler.cc", "line": 45}] - } - }, - "statistics": { - "total_metrics": 150, - "by_type": {"counter": 75, "gauge": 60, "histogram": 15}, - "by_constructor": {"make_counter": 75, "make_gauge": 60, "make_histogram": 15}, - "with_description": 140, - "with_labels": 85 - } -} ----- - -=== AsciiDoc Output (`metrics.adoc`) - -Human-readable documentation in AsciiDoc format: - -[source,asciidoc] ----- -=== redpanda_kafka_requests_total - -Total number of Kafka requests - -*Type*: counter +The tool automatically extracts metrics created with these constructors: -*Labels*: +* `sm::make_gauge`, `sm::make_counter`, `sm::make_histogram` +* `sm::make_total_bytes`, `sm::make_derive` +* `ss::metrics::make_total_operations`, `ss::metrics::make_current_bytes` -- `request_type` -- `status` - -*Source*: `src/v/kafka/server/handlers/handler.cc` - ---- ----- - -== Command Line Options - -=== metrics_extractor.py - -[source,bash] ----- -python metrics_extractor.py [OPTIONS] PATH +For each metric, it extracts: +* Name and type (gauge, counter, histogram) +* Description and labels +* Classification (internal vs external) +* Source location +== Prerequisites -Arguments: - PATH Path to Redpanda source directory +* Python 3.8+ and build tools (for tree-sitter compilation) +* Git -Options: - -r, --recursive Search for C++ files recursively - -o, --output FILE Output JSON file (default: metrics.json) - -a, --asciidoc FILE Generate AsciiDoc output file - --filter-namespace NS Filter metrics by namespace (e.g., redpanda) - --treesitter-dir DIR Tree-sitter directory (default: tree-sitter) - --treesitter-lib FILE Tree-sitter library file - -v, --verbose Enable verbose logging - -h, --help Show help message ----- +== Development Commands -=== compare_metrics.py +For developers working on the tool itself: [source,bash] ---- -python compare_metrics.py [OPTIONS] METRICS_FILE - -Arguments: - METRICS_FILE JSON file with extracted metrics - -Options: - --existing FILE Existing metrics JSON for comparison - --output FILE Output comparison report to JSON - -v, --verbose Verbose logging - -h, --help Show help message ----- - -== Integration with Documentation Pipeline - -The metrics extractor integrates with the existing documentation automation: - -=== Adding to doc-tools.js - -Add the metrics extraction command to the CLI: - -[source,javascript] ----- -automation - .command('metric-docs') - .description('Generate metrics documentation from Redpanda source code') - .option('--tag ', 'Git tag or branch to extract from', 'dev') - .action((options) => { - const cwd = path.resolve(__dirname, '../tools/metrics-extractor'); - const make = spawnSync('make', ['build', `TAG=${options.tag}`], - { cwd, stdio: 'inherit' }); - if (make.status !== 0) process.exit(make.status); - }); ----- - -=== Autogenerated Directory Structure - -The extractor follows the same pattern as property-extractor: - ----- -autogenerated/ -β”œβ”€β”€ v23.3.1/ -β”‚ └── metrics/ -β”‚ β”œβ”€β”€ metrics.json -β”‚ └── metrics.adoc -└── dev/ - └── metrics/ - β”œβ”€β”€ metrics.json - └── metrics.adoc ----- - -== Development - -=== Running Tests +# Set up development environment +make setup-venv +make install-deps -[source,bash] ----- +# Run tests make test ----- - -=== Code Formatting - -[source,bash] ----- -make format ----- - -=== Linting - -[source,bash] ----- -make lint ----- - -== Tree-sitter Queries - -The extractor uses sophisticated tree-sitter queries to identify metric constructors. Here's an example query for `sm::make_gauge`: - -[source,scheme] ----- -(call_expression - function: (qualified_identifier - scope: (namespace_identifier) @namespace - name: (identifier) @function_name - (#match? @namespace "sm") - (#match? @function_name "make_gauge") - ) - arguments: (argument_list - (string_literal) @metric_name - . * - (call_expression - function: (qualified_identifier - scope: (namespace_identifier) @desc_namespace - name: (identifier) @desc_function - (#match? @desc_namespace "sm") - (#match? @desc_function "description") - ) - arguments: (argument_list - (string_literal) @description - ) - )? - ) -) ----- - -== Troubleshooting - -=== Tree-sitter Compilation Issues - -If tree-sitter fails to compile: - -1. Ensure you have build tools installed: - * Linux: `sudo apt install build-essential` - * macOS: `xcode-select --install` - * Windows: Install Visual Studio Build Tools -2. Check Python version (3.8+ required) - -3. Clear tree-sitter cache: -+ -[source,bash] ----- -make clean ----- - -=== Missing Metrics - -If expected metrics are not found: - -1. Check if the constructor patterns match the query -2. Verify file paths and namespaces -3. Enable verbose logging to see parsing details -4. Check for C++ syntax errors in source files - -=== Performance Issues - -For large codebases: - -1. Use `--filter-namespace` to limit scope -2. Process specific directories instead of entire codebase -3. Increase system memory if available - -== Contributing - -When adding support for new metric constructors: - -1. Add the tree-sitter query to `METRICS_QUERIES` in `metrics_parser.py` -2. Update `FUNCTION_TO_TYPE` mapping -3. Add tests for the new constructor pattern -4. Update documentation - -== Examples - -=== Extract Specific Namespace - -[source,bash] ----- -python metrics_extractor.py /path/to/redpanda/src \ - --filter-namespace redpanda \ - --recursive \ - --output redpanda_metrics.json ----- - -=== Compare with Previous Version - -[source,bash] ----- -# Extract current metrics -make build TAG=dev - -# Extract previous version -make build TAG=v23.3.1 - -# Compare -python compare_metrics.py autogenerated/dev/metrics/metrics.json \ - --existing autogenerated/v23.3.1/metrics/metrics.json \ - --output comparison_report.json ----- - -=== Generate Documentation Only - -[source,bash] ----- -python metrics_extractor.py /path/to/redpanda/src \ - --asciidoc redpanda_metrics.adoc \ - --filter-namespace redpanda +# Manual extraction (development) +python metrics_extractor.py --redpanda-repo /path/to/redpanda/src --verbose ---- diff --git a/tools/metrics-extractor/example.py b/tools/metrics-extractor/example.py deleted file mode 100644 index b990f7b..0000000 --- a/tools/metrics-extractor/example.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env python3 -""" -Example script demonstrating the metrics extractor usage -""" -import argparse -import tempfile -import os -from pathlib import Path - -# Sample C++ code with metrics that would be found in Redpanda -REDPANDA_SAMPLE = ''' -// File: src/v/kafka/server/handlers/produce.cc -#include - -namespace kafka { - -class produce_handler { -private: - void setup_metrics() { - _metrics.add_group("kafka", { - sm::make_counter( - "produce_requests_total", - [this] { return _produce_requests; }, - sm::description("Total number of produce requests received")), - - sm::make_gauge( - "active_connections", - [this] { return _connections.size(); }, - sm::description("Number of currently active Kafka connections")), - - sm::make_histogram( - "produce_latency_seconds", - sm::description("Latency histogram for Kafka produce requests")), - - sm::make_total_bytes( - "bytes_produced_total", - [this] { return _bytes_produced; }, - sm::description("Total bytes produced to Kafka topics")) - }); - } - - uint64_t _produce_requests = 0; - uint64_t _bytes_produced = 0; - std::vector _connections; - ss::metrics::metric_groups _metrics; -}; - -} // namespace kafka -''' - -CLUSTER_SAMPLE = ''' -// File: src/v/cluster/partition_manager.cc -#include - -namespace cluster { - -class partition_manager { -private: - void register_metrics() { - _metrics.add_group("redpanda_cluster", { - sm::make_gauge( - "partitions", - [this] { return _partitions.size(); }, - sm::description("Number of partitions in the cluster")), - - sm::make_counter( - "leadership_changes", - [this] { return _leadership_changes; }, - sm::description("Number of leadership changes across all partitions")), - - ss::metrics::make_current_bytes( - "memory_usage_bytes", - [this] { return _memory_tracker.used(); }, - ss::metrics::description("Current memory usage by partition manager")) - }); - } - - std::unordered_map _partitions; - uint64_t _leadership_changes = 0; - memory_tracker _memory_tracker; - ss::metrics::metric_groups _metrics; -}; - -} // namespace cluster -''' - - -def create_sample_files(temp_dir): - """Create sample C++ files for testing""" - files = [] - - # Create kafka handler file - kafka_file = temp_dir / "kafka_produce.cc" - with open(kafka_file, 'w') as f: - f.write(REDPANDA_SAMPLE) - files.append(kafka_file) - - # Create cluster manager file - cluster_file = temp_dir / "cluster_partition_manager.cc" - with open(cluster_file, 'w') as f: - f.write(CLUSTER_SAMPLE) - files.append(cluster_file) - - return files - - -def run_example(): - """Run the metrics extraction example""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - print("πŸ”§ Creating sample C++ files...") - sample_files = create_sample_files(temp_path) - - print("πŸ“ Sample files created:") - for f in sample_files: - print(f" β€’ {f.name}") - - try: - from metrics_parser import get_treesitter_cpp_parser_and_language, parse_cpp_file - from metrics_bag import MetricsBag - - print("\n🌳 Initializing tree-sitter C++ parser...") - parser, language = get_treesitter_cpp_parser_and_language("tree-sitter", "tree-sitter-cpp.so") - - print("πŸ” Extracting metrics from sample files...") - all_metrics = MetricsBag() - - for cpp_file in sample_files: - print(f" Processing {cpp_file.name}...") - file_metrics = parse_cpp_file(cpp_file, parser, language) - all_metrics.merge(file_metrics) - - # Display results - print(f"\nβœ… Extraction completed! Found {len(all_metrics)} metrics:") - print() - - for name, metric in all_metrics.get_all_metrics().items(): - print(f"πŸ“Š {name}") - print(f" Type: {metric['type']}") - print(f" Description: {metric.get('description', 'No description')}") - if metric.get('labels'): - print(f" Labels: {', '.join(metric['labels'])}") - print(f" Constructor: {metric['constructor']}") - print(f" File: {metric['files'][0]['file']}") - print() - - # Show statistics - stats = all_metrics.get_statistics() - print("πŸ“ˆ Statistics:") - print(f" Total metrics: {stats['total_metrics']}") - print(f" By type: {stats['by_type']}") - print(f" By constructor: {stats['by_constructor']}") - print(f" With descriptions: {stats['with_description']}") - print() - - return True - - except ImportError as e: - print(f"❌ Import error: {e}") - print("Make sure to install dependencies with: pip install -r requirements.txt") - return False - except Exception as e: - print(f"❌ Error during extraction: {e}") - return False - - -def main(): - parser = argparse.ArgumentParser(description="Redpanda metrics extractor example") - parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output") - - args = parser.parse_args() - - if args.verbose: - import logging - logging.basicConfig(level=logging.DEBUG) - - print("πŸš€ Redpanda Metrics Extractor Example") - print("=====================================") - print() - print("This example demonstrates how the metrics extractor works") - print("by processing sample C++ code with Redpanda metrics.") - print() - - success = run_example() - - if success: - print("πŸŽ‰ Example completed successfully!") - print() - print("Next steps:") - print(" 1. Run against real Redpanda source: make build TAG=dev") - print(" 2. Compare with existing metrics: python compare_metrics.py metrics.json") - print(" 3. Generate documentation: see README.adoc for details") - else: - print("❌ Example failed. Please check the error messages above.") - return 1 - - return 0 - - -if __name__ == "__main__": - exit(main()) diff --git a/tools/metrics-extractor/metrics.json b/tools/metrics-extractor/metrics.json deleted file mode 100644 index f2720a2..0000000 --- a/tools/metrics-extractor/metrics.json +++ /dev/null @@ -1,5036 +0,0 @@ -{ - "metrics": { - "puts": { - "name": "puts", - "type": "counter", - "description": "Total number of files put into cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 27 - } - ] - }, - "gets": { - "name": "gets", - "type": "counter", - "description": "Total number of cache get requests.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 31 - } - ] - }, - "cached_gets": { - "name": "cached_gets", - "type": "counter", - "description": "Total number of get requests that are already in cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 35 - } - ] - }, - "size_bytes": { - "name": "size_bytes", - "type": "gauge", - "description": "Current cache size in bytes.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 40 - } - ] - }, - "files": { - "name": "files", - "type": "gauge", - "description": "Current number of files in cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 44 - } - ] - }, - "in_progress_files": { - "name": "in_progress_files", - "type": "gauge", - "description": "Current number of files that are being put to cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 48 - } - ] - }, - "hwm_size_bytes": { - "name": "hwm_size_bytes", - "type": "gauge", - "description": "High watermark of sum of size of cached objects.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 69 - } - ] - }, - "hwm_files": { - "name": "hwm_files", - "type": "gauge", - "description": "High watermark of number of objects in cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 80 - } - ] - }, - "tracker_syncs": { - "name": "tracker_syncs", - "type": "counter", - "description": "Number of times the access tracker was updated ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 86 - } - ] - }, - "tracker_size": { - "name": "tracker_size", - "type": "gauge", - "description": "Number of entries in cache access tracker", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 93 - } - ] - }, - "fast_trims": { - "name": "fast_trims", - "type": "counter", - "description": "Number of times we have trimmed the cache ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 103 - } - ] - }, - "exhaustive_trims": { - "name": "exhaustive_trims", - "type": "counter", - "description": "Number of times we couldn't free enough space with a fast ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 109 - } - ] - }, - "carryover_trims": { - "name": "carryover_trims", - "type": "counter", - "description": "Number of times we invoked carryover trim.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 116 - } - ] - }, - "failed_trims": { - "name": "failed_trims", - "type": "counter", - "description": "Number of times could not free the expected amount of ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 121 - } - ] - }, - "in_mem_trims": { - "name": "in_mem_trims", - "type": "counter", - "description": "Number of times we trimmed the cache using ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 128 - } - ] - }, - "put": { - "name": "put", - "type": "counter", - "description": "Number of objects written into cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 141 - } - ] - }, - "hit": { - "name": "hit", - "type": "counter", - "description": "Number of get requests for objects that are ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 146 - } - ] - }, - "miss": { - "name": "miss", - "type": "counter", - "description": "Number of failed get requests because of ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 152 - } - ] - }, - "read_bytes": { - "name": "read_bytes", - "type": "counter", - "description": "Total bytes read from remote partition", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 36 - }, - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 123 - }, - { - "file": "tmp/redpanda-dev/src/v/transform/probe.cc", - "line": 31 - } - ] - }, - "read_records": { - "name": "read_records", - "type": "counter", - "description": "Total number of records read from remote partition", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 42 - } - ] - }, - "chunk_size": { - "name": "chunk_size", - "type": "gauge", - "description": "Size of chunk downloaded from cloud storage", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 48 - } - ] - }, - "downloads_throttled_sum": { - "name": "downloads_throttled_sum", - "type": "counter", - "description": "Total amount of time downloads were throttled (ms)", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 66 - } - ] - }, - "materialized_segments": { - "name": "materialized_segments", - "type": "gauge", - "description": "Current number of materialized remote segments", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 79 - } - ] - }, - "readers": { - "name": "readers", - "type": "gauge", - "description": "Current number of remote partition readers", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 85 - }, - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 206 - } - ] - }, - "spillover_manifest_bytes": { - "name": "spillover_manifest_bytes", - "type": "gauge", - "description": "Total amount of memory used by spillover manifests", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 91 - } - ] - }, - "spillover_manifest_instances": { - "name": "spillover_manifest_instances", - "type": "gauge", - "description": "Total number of spillover manifests stored in memory", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 98 - } - ] - }, - "spillover_manifest_hydrated": { - "name": "spillover_manifest_hydrated", - "type": "counter", - "description": "Number of times spillover manifests were saved to the cache", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 105 - } - ] - }, - "spillover_manifest_materialized": { - "name": "spillover_manifest_materialized", - "type": "counter", - "description": "Number of times spillover manifests were loaded ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 112 - } - ] - }, - "segment_readers": { - "name": "segment_readers", - "type": "gauge", - "description": "Current number of remote segment readers", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 119 - } - ] - }, - "spillover_manifest_latency": { - "name": "spillover_manifest_latency", - "type": "histogram", - "description": "Spillover manifest materialization latency histogram", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 125 - } - ] - }, - "chunks_hydrated": { - "name": "chunks_hydrated", - "type": "counter", - "description": "Total number of hydrated chunks (some may have been ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 134 - } - ] - }, - "chunk_hydration_latency": { - "name": "chunk_hydration_latency", - "type": "histogram", - "description": "Chunk hydration latency histogram", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 142 - } - ] - }, - "hydrations_in_progress": { - "name": "hydrations_in_progress", - "type": "counter", - "description": "Active hydrations in progress", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/read_path_probes.cc", - "line": 149 - } - ] - }, - "topic_manifest_uploads": { - "name": "topic_manifest_uploads", - "type": "counter", - "description": "Number of topic manifest uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 34 - } - ] - }, - "partition_manifest_uploads": { - "name": "partition_manifest_uploads", - "type": "counter", - "description": "Number of partition manifest (re)uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 38 - } - ] - }, - "topic_manifest_downloads": { - "name": "topic_manifest_downloads", - "type": "counter", - "description": "Number of topic manifest downloads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 42 - } - ] - }, - "partition_manifest_downloads": { - "name": "partition_manifest_downloads", - "type": "counter", - "description": "Number of partition manifest downloads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 46 - } - ] - }, - "cluster_metadata_manifest_uploads": { - "name": "cluster_metadata_manifest_uploads", - "type": "counter", - "description": "Number of partition manifest uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 50 - } - ] - }, - "cluster_metadata_manifest_downloads": { - "name": "cluster_metadata_manifest_downloads", - "type": "counter", - "description": "Number of partition manifest downloads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 54 - } - ] - }, - "manifest_upload_backoff": { - "name": "manifest_upload_backoff", - "type": "counter", - "description": "Number of times backoff was applied during manifest upload", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 58 - } - ] - }, - "manifest_download_backoff": { - "name": "manifest_download_backoff", - "type": "counter", - "description": "Number of times backoff was applied during ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 63 - } - ] - }, - "successful_uploads": { - "name": "successful_uploads", - "type": "counter", - "description": "Number of completed log-segment uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 68 - } - ] - }, - "successful_downloads": { - "name": "successful_downloads", - "type": "counter", - "description": "Number of completed log-segment downloads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 72 - } - ] - }, - "failed_uploads": { - "name": "failed_uploads", - "type": "counter", - "description": "Number of failed log-segment uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 76 - } - ] - }, - "failed_downloads": { - "name": "failed_downloads", - "type": "counter", - "description": "Number of failed log-segment downloads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 80 - } - ] - }, - "failed_manifest_uploads": { - "name": "failed_manifest_uploads", - "type": "counter", - "description": "Number of failed manifest uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 84 - } - ] - }, - "failed_manifest_downloads": { - "name": "failed_manifest_downloads", - "type": "counter", - "description": "Number of failed manifest downloads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 88 - } - ] - }, - "upload_backoff": { - "name": "upload_backoff", - "type": "counter", - "description": "Number of times backoff was applied during ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 92 - }, - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 257 - } - ] - }, - "download_backoff": { - "name": "download_backoff", - "type": "counter", - "description": "Number of times backoff was applied during ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 97 - }, - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 251 - } - ] - }, - "bytes_sent": { - "name": "bytes_sent", - "type": "counter", - "description": "Number of bytes sent to cloud storage", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 102 - }, - { - "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", - "line": 142 - } - ] - }, - "bytes_received": { - "name": "bytes_received", - "type": "counter", - "description": "Number of bytes received from cloud storage", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 106 - }, - { - "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", - "line": 135 - } - ] - }, - "index_uploads": { - "name": "index_uploads", - "type": "counter", - "description": "Number of segment indices uploaded", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 110 - } - ] - }, - "index_downloads": { - "name": "index_downloads", - "type": "counter", - "description": "Number of segment indices downloaded", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 114 - } - ] - }, - "failed_index_uploads": { - "name": "failed_index_uploads", - "type": "counter", - "description": "Number of failed segment index uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 118 - } - ] - }, - "failed_index_downloads": { - "name": "failed_index_downloads", - "type": "counter", - "description": "Number of failed segment index downloads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 122 - } - ] - }, - "spillover_manifest_uploads": { - "name": "spillover_manifest_uploads", - "type": "counter", - "description": "Number of spillover manifest (re)uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 126 - } - ] - }, - "spillover_manifest_downloads": { - "name": "spillover_manifest_downloads", - "type": "counter", - "description": "Number of spillover manifest downloads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 130 - } - ] - }, - "controller_snapshot_successful_uploads": { - "name": "controller_snapshot_successful_uploads", - "type": "counter", - "description": "Number of completed controller snapshot uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 134 - } - ] - }, - "controller_snapshot_failed_uploads": { - "name": "controller_snapshot_failed_uploads", - "type": "counter", - "description": "Number of failed controller snapshot uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 139 - } - ] - }, - "controller_snapshot_upload_backoff": { - "name": "controller_snapshot_upload_backoff", - "type": "counter", - "description": "Number of times backoff was applied during ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 143 - } - ] - }, - "client_acquisition_latency": { - "name": "client_acquisition_latency", - "type": "histogram", - "description": "Client acquisition latency histogram", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 148 - } - ] - }, - "segment_download_latency": { - "name": "segment_download_latency", - "type": "histogram", - "description": "Segment download latency histogram", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 154 - } - ] - }, - "errors_total": { - "name": "errors_total", - "type": "counter", - "description": "Number of transmit errors", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 169 - }, - { - "file": "tmp/redpanda-dev/src/v/security/oidc_service.cc", - "line": 99 - }, - { - "file": "tmp/redpanda-dev/src/v/security/audit/probes.cc", - "line": 40 - } - ] - }, - "partition_manifest_uploads_total": { - "name": "partition_manifest_uploads_total", - "type": "counter", - "description": "Successful partition manifest uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 188 - } - ] - }, - "segment_uploads_total": { - "name": "segment_uploads_total", - "type": "counter", - "description": "Successful data segment uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 194 - } - ] - }, - "active_segments": { - "name": "active_segments", - "type": "gauge", - "description": "Number of remote log segments currently hydrated for read", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 200 - } - ] - }, - "partition_readers": { - "name": "partition_readers", - "type": "gauge", - "description": "Number of partition reader instances (number of current ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 212 - } - ] - }, - "partition_readers_delayed": { - "name": "partition_readers_delayed", - "type": "counter", - "description": "How many partition reades were delayed due to ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 219 - } - ] - }, - "segment_readers_delayed": { - "name": "segment_readers_delayed", - "type": "counter", - "description": "How many segment readers were delayed due to ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 226 - } - ] - }, - "segment_materializations_delayed": { - "name": "segment_materializations_delayed", - "type": "counter", - "description": "How many segment materializations were delayed due to ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 233 - } - ] - }, - "segment_index_uploads_total": { - "name": "segment_index_uploads_total", - "type": "counter", - "description": "Successful segment index uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 241 - } - ] - }, - "spillover_manifest_uploads_total": { - "name": "spillover_manifest_uploads_total", - "type": "counter", - "description": "Successful spillover manifest uploads", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 247 - } - ] - }, - "spillover_manifests_materialized_count": { - "name": "spillover_manifests_materialized_count", - "type": "gauge", - "description": "How many spilled manifests are currently cached in memory", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 253 - } - ] - }, - "spillover_manifests_materialized_bytes": { - "name": "spillover_manifests_materialized_bytes", - "type": "gauge", - "description": "Bytes of memory used for spilled manifests ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/remote_probe.cc", - "line": 260 - } - ] - }, - "total_uploads": { - "name": "total_uploads", - "type": "counter", - "description": "Number of completed PUT requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 125 - } - ] - }, - "total_downloads": { - "name": "total_downloads", - "type": "counter", - "description": "Number of completed GET requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 130 - } - ] - }, - "all_requests": { - "name": "all_requests", - "type": "counter", - "description": "Number of completed HTTP requests (includes PUT and GET)", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 135 - } - ] - }, - "active_uploads": { - "name": "active_uploads", - "type": "gauge", - "description": "Number of active PUT requests at the moment", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 141 - } - ] - }, - "active_downloads": { - "name": "active_downloads", - "type": "gauge", - "description": "Number of active GET requests at the moment", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 146 - } - ] - }, - "active_requests": { - "name": "active_requests", - "type": "gauge", - "description": "Number of active HTTP requests at the moment ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 151 - }, - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 72 - } - ] - }, - "total_inbound_bytes": { - "name": "total_inbound_bytes", - "type": "counter", - "description": "Total number of bytes received from cloud storage", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 157 - }, - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 79 - } - ] - }, - "total_outbound_bytes": { - "name": "total_outbound_bytes", - "type": "counter", - "description": "Total number of bytes sent to cloud storage", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 162 - }, - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 86 - } - ] - }, - "num_rpc_errors": { - "name": "num_rpc_errors", - "type": "counter", - "description": "Total number of REST API errors received from ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 167 - } - ] - }, - "num_transport_errors": { - "name": "num_transport_errors", - "type": "counter", - "description": "Total number of transport errors (TCP and TLS)", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 173 - }, - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 93 - } - ] - }, - "num_slowdowns": { - "name": "num_slowdowns", - "type": "counter", - "description": "Total number of SlowDown errors received from cloud ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 178 - } - ] - }, - "num_nosuchkey": { - "name": "num_nosuchkey", - "type": "counter", - "description": "Total number of NoSuchKey errors received from cloud ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 184 - } - ] - }, - "num_borrows": { - "name": "num_borrows", - "type": "counter", - "description": "Number of time current shard had to borrow a cloud ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 191 - } - ] - }, - "lease_duration": { - "name": "lease_duration", - "type": "histogram", - "description": "Lease duration histogram", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 197 - } - ] - }, - "client_pool_utilization": { - "name": "client_pool_utilization", - "type": "gauge", - "description": "Utilization of the cloud storage pool(0 - unused, ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 202 - } - ] - }, - "lease_timeouts_total": { - "name": "lease_timeouts_total", - "type": "counter", - "description": "Number of cloud storage client lease timeouts, usually indicating ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 208 - } - ] - }, - "not_found": { - "name": "not_found", - "type": "counter", - "description": "Total number of requests for which the object was not found", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 238 - } - ] - }, - "backoff": { - "name": "backoff", - "type": "counter", - "description": "Total number of requests that backed off", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 245 - } - ] - }, - "uploads": { - "name": "uploads", - "type": "counter", - "description": "Total number of requests that uploaded an object to ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 263 - } - ] - }, - "downloads": { - "name": "downloads", - "type": "counter", - "description": "Total number of requests that downloaded an object ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage_clients/client_probe.cc", - "line": 270 - } - ] - }, - "pending_partition_operations": { - "name": "pending_partition_operations", - "type": "gauge", - "description": "Number of partitions with ongoing/requested operations", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/controller_backend.cc", - "line": 341 - } - ] - }, - "requests_dropped": { - "name": "requests_dropped", - "type": "counter", - "description": "Controller log rate limiting. ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/controller_log_limiter.cc", - "line": 69 - } - ] - }, - "requests_available_rps": { - "name": "requests_available_rps", - "type": "gauge", - "description": "Controller log rate limiting.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/controller_log_limiter.cc", - "line": 77 - } - ] - }, - "brokers": { - "name": "brokers", - "type": "gauge", - "description": "Number of configured brokers in the cluster", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", - "line": 72 - } - ] - }, - "topics": { - "name": "topics", - "type": "gauge", - "description": "Number of topics in the cluster", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", - "line": 81 - } - ] - }, - "partitions": { - "name": "partitions", - "type": "gauge", - "description": "Number of partitions in the cluster (replicas not included)", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", - "line": 89 - }, - { - "file": "tmp/redpanda-dev/src/v/cluster/topic_table_probe.cc", - "line": 163 - } - ] - }, - "unavailable_partitions": { - "name": "unavailable_partitions", - "type": "gauge", - "description": "Number of partitions that lack quorum among replicants", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", - "line": 97 - } - ] - }, - "non_homogenous_fips_mode": { - "name": "non_homogenous_fips_mode", - "type": "gauge", - "description": "Number of nodes that have a non-homogenous FIPS mode value", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", - "line": 108 - } - ] - }, - "latest_cluster_metadata_manifest_age": { - "name": "latest_cluster_metadata_manifest_age", - "type": "gauge", - "description": "Age in seconds of the latest ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/controller_probe.cc", - "line": 132 - } - ] - }, - "queued_node_operations": { - "name": "queued_node_operations", - "type": "gauge", - "description": "Number of queued node operations", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/members_backend.cc", - "line": 72 - } - ] - }, - "rpcs_sent": { - "name": "rpcs_sent", - "type": "gauge", - "description": "Number of node status RPCs sent by this node", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/node_status_backend.cc", - "line": 366 - } - ] - }, - "rpcs_timed_out": { - "name": "rpcs_timed_out", - "type": "gauge", - "description": "Number of timed out node status RPCs from this node", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/node_status_backend.cc", - "line": 371 - } - ] - }, - "rpcs_received": { - "name": "rpcs_received", - "type": "gauge", - "description": "Number of node status RPCs received by this node", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/node_status_backend.cc", - "line": 377 - } - ] - }, - "num_with_broken_rack_constraint": { - "name": "num_with_broken_rack_constraint", - "type": "gauge", - "description": "Number of partitions that don't satisfy the rack ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_balancer_state.cc", - "line": 156 - } - ] - }, - "leader_id": { - "name": "leader_id", - "type": "gauge", - "description": "Id of current partition leader", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 83 - } - ] - }, - "under_replicated_replicas": { - "name": "under_replicated_replicas", - "type": "gauge", - "description": "Number of under replicated replicas", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 91 - } - ] - }, - "leader": { - "name": "leader", - "type": "gauge", - "description": "Flag indicating if this partition instance is a leader", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 104 - } - ] - }, - "start_offset": { - "name": "start_offset", - "type": "gauge", - "description": "start offset", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 110 - } - ] - }, - "last_stable_offset": { - "name": "last_stable_offset", - "type": "gauge", - "description": "Last stable offset", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 115 - } - ] - }, - "committed_offset": { - "name": "committed_offset", - "type": "gauge", - "description": "Partition commited offset. i.e. safely persisted on ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 120 - } - ] - }, - "end_offset": { - "name": "end_offset", - "type": "gauge", - "description": "Last offset stored by current partition on this node", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 126 - } - ] - }, - "high_watermark": { - "name": "high_watermark", - "type": "gauge", - "description": "Partion high watermark i.e. highest consumable offset", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 132 - } - ] - }, - "records_produced": { - "name": "records_produced", - "type": "counter", - "description": "Total number of records produced", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 138 - } - ] - }, - "batches_produced": { - "name": "batches_produced", - "type": "gauge", - "description": "Total number of batches produced", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 143 - } - ] - }, - "records_fetched": { - "name": "records_fetched", - "type": "counter", - "description": "Total number of records fetched", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 148 - } - ] - }, - "bytes_produced_total": { - "name": "bytes_produced_total", - "type": "counter", - "description": "Total number of bytes produced", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 153 - } - ] - }, - "bytes_fetched_total": { - "name": "bytes_fetched_total", - "type": "counter", - "description": "Total number of bytes fetched (not all might be ", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 158 - } - ] - }, - "bytes_fetched_from_follower_total": { - "name": "bytes_fetched_from_follower_total", - "type": "counter", - "description": "Total number of bytes fetched from follower (not all might be ", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 164 - } - ] - }, - "cloud_storage_segments_metadata_bytes": { - "name": "cloud_storage_segments_metadata_bytes", - "type": "counter", - "description": "Current number of bytes consumed by remote segments ", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 171 - } - ] - }, - "iceberg_offsets_pending_translation": { - "name": "iceberg_offsets_pending_translation", - "type": "gauge", - "description": "Total number of offsets that are pending ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 196 - } - ] - }, - "iceberg_offsets_pending_commit": { - "name": "iceberg_offsets_pending_commit", - "type": "gauge", - "description": "Total number of offsets that are pending ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 210 - } - ] - }, - "schema_id_validation_records_failed": { - "name": "schema_id_validation_records_failed", - "type": "counter", - "description": "Number of records that failed schema ID validation", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 234 - } - ] - }, - "max_offset": { - "name": "max_offset", - "type": "gauge", - "description": "Latest readable offset of the partition (i.e. high watermark)", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 272 - } - ] - }, - "request_bytes_total": { - "name": "request_bytes_total", - "type": "counter", - "description": "produce", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 314 - } - ] - }, - "records_produced_total": { - "name": "records_produced_total", - "type": "counter", - "description": "Total number of records produced", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 332 - } - ] - }, - "records_fetched_total": { - "name": "records_fetched_total", - "type": "counter", - "description": "Total number of records fetched", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 338 - } - ] - }, - "anomalies": { - "name": "anomalies", - "type": "gauge", - "description": "", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/partition_probe.cc", - "line": 407 - } - ] - }, - "producer_manager_total_active_producers": { - "name": "producer_manager_total_active_producers", - "type": "gauge", - "description": "Total number of active idempotent and transactional producers.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/producer_state_manager.cc", - "line": 61 - } - ] - }, - "evicted_producers": { - "name": "evicted_producers", - "type": "counter", - "description": "Number of evicted producers so far.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/producer_state_manager.cc", - "line": 66 - } - ] - }, - "idempotency_pid_cache_size": { - "name": "idempotency_pid_cache_size", - "type": "gauge", - "description": "Number of active producers (known producer_id seq number pairs).", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/rm_stm.cc", - "line": 2212 - } - ] - }, - "tx_num_inflight_requests": { - "name": "tx_num_inflight_requests", - "type": "gauge", - "description": "Number of ongoing transactional requests.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/rm_stm.cc", - "line": 2218 - } - ] - }, - "assigned_partitions": { - "name": "assigned_partitions", - "type": "gauge", - "description": "Number of partitions assigned to this shard", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/shard_placement_table_probe.cc", - "line": 25 - } - ] - }, - "hosted_partitions": { - "name": "hosted_partitions", - "type": "gauge", - "description": "Number of partitions hosted on this shard", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/shard_placement_table_probe.cc", - "line": 29 - } - ] - }, - "partitions_to_reconcile": { - "name": "partitions_to_reconcile", - "type": "gauge", - "description": "Number of partitions needing reconciliation of ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/shard_placement_table_probe.cc", - "line": 33 - } - ] - }, - "remade_partitions": { - "name": "remade_partitions", - "type": "gauge", - "description": "Number of partitions that were forced to be remade", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/shard_placement_table_probe.cc", - "line": 38 - } - ] - }, - "moving_to_node": { - "name": "moving_to_node", - "type": "gauge", - "description": "Amount of partitions that are moving to node", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/topic_table_probe.cc", - "line": 45 - } - ] - }, - "moving_from_node": { - "name": "moving_from_node", - "type": "gauge", - "description": "Amount of partitions that are moving from node", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/topic_table_probe.cc", - "line": 50 - } - ] - }, - "node_cancelling_movements": { - "name": "node_cancelling_movements", - "type": "gauge", - "description": "Amount of cancelling partition movements for node", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/topic_table_probe.cc", - "line": 55 - } - ] - }, - "replicas": { - "name": "replicas", - "type": "gauge", - "description": "Configured number of replicas for the topic", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/topic_table_probe.cc", - "line": 150 - } - ] - }, - "uploaded": { - "name": "uploaded", - "type": "counter", - "description": "Uploaded offsets", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 58 - } - ] - }, - "uploaded_bytes": { - "name": "uploaded_bytes", - "type": "counter", - "description": "Total number of uploaded bytes", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 63 - } - ] - }, - "missing": { - "name": "missing", - "type": "counter", - "description": "Missing offsets due to gaps", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 68 - } - ] - }, - "pending": { - "name": "pending", - "type": "gauge", - "description": "Pending offsets", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 73 - } - ] - }, - "compacted_replaced_bytes": { - "name": "compacted_replaced_bytes", - "type": "gauge", - "description": "Bytes replaced due to compaction since this replica ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 78 - } - ] - }, - "deleted_segments": { - "name": "deleted_segments", - "type": "counter", - "description": "Number of segments that have been deleted from S3 for the topic. ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 115 - } - ] - }, - "segments": { - "name": "segments", - "type": "gauge", - "description": "Total number of accounted segments in the cloud for the topic", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 124 - } - ] - }, - "segments_pending_deletion": { - "name": "segments_pending_deletion", - "type": "gauge", - "description": "Total number of segments pending deletion from the ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 131 - } - ] - }, - "cloud_log_size": { - "name": "cloud_log_size", - "type": "gauge", - "description": "Total size in bytes of the user-visible log for the topic", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 154 - } - ] - }, - "paused_archivers": { - "name": "paused_archivers", - "type": "gauge", - "description": "Number of paused archivers", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 161 - } - ] - }, - "rounds": { - "name": "rounds", - "type": "counter", - "description": "Number of upload housekeeping rounds", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 179 - } - ] - }, - "jobs_completed": { - "name": "jobs_completed", - "type": "counter", - "description": "Number of executed housekeeping jobs", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 184 - } - ] - }, - "jobs_failed": { - "name": "jobs_failed", - "type": "counter", - "description": "Number of failed housekeeping jobs", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 189 - } - ] - }, - "jobs_skipped": { - "name": "jobs_skipped", - "type": "counter", - "description": "Number of skipped housekeeping jobs", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 194 - } - ] - }, - "resumes": { - "name": "resumes", - "type": "gauge", - "description": "Number of times upload housekeeping was resumed", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 199 - } - ] - }, - "pauses": { - "name": "pauses", - "type": "gauge", - "description": "Number of times upload housekeeping was paused", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 204 - } - ] - }, - "drains": { - "name": "drains", - "type": "gauge", - "description": "Number of times upload housekeeping queue was drained", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 209 - } - ] - }, - "requests_throttled_average_rate": { - "name": "requests_throttled_average_rate", - "type": "gauge", - "description": "Average rate of requests from the read and write ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 215 - } - ] - }, - "local_segment_reuploads": { - "name": "local_segment_reuploads", - "type": "gauge", - "description": "Number of segment reuploads from local data directory", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 225 - } - ] - }, - "cloud_segment_reuploads": { - "name": "cloud_segment_reuploads", - "type": "gauge", - "description": "Number of segment reuploads from cloud storage sources (cloud ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 231 - } - ] - }, - "manifest_reuploads": { - "name": "manifest_reuploads", - "type": "gauge", - "description": "Number of manifest reuploads performed by all housekeeping jobs", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 238 - } - ] - }, - "segment_deletions": { - "name": "segment_deletions", - "type": "gauge", - "description": "Number of segments deleted by all housekeeping jobs", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 244 - } - ] - }, - "metadata_syncs": { - "name": "metadata_syncs", - "type": "gauge", - "description": "Number of archival configuration updates performed ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/archival/probe.cc", - "line": 250 - } - ] - }, - "leader_transfer_error": { - "name": "leader_transfer_error", - "type": "counter", - "description": "Number of errors attempting to transfer leader", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/scheduling/leader_balancer_probe.cc", - "line": 25 - } - ] - }, - "leader_transfer_succeeded": { - "name": "leader_transfer_succeeded", - "type": "counter", - "description": "Number of successful leader transfers", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/scheduling/leader_balancer_probe.cc", - "line": 29 - } - ] - }, - "leader_transfer_timeout": { - "name": "leader_transfer_timeout", - "type": "counter", - "description": "Number of timeouts attempting to transfer leader", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/scheduling/leader_balancer_probe.cc", - "line": 33 - } - ] - }, - "leader_transfer_no_improvement": { - "name": "leader_transfer_no_improvement", - "type": "counter", - "description": "Number of times no balance improvement was found", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cluster/scheduling/leader_balancer_probe.cc", - "line": 37 - } - ] - }, - "backlog_size": { - "name": "backlog_size", - "type": "gauge", - "description": "Iceberg controller current backlog - averaged size ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/backlog_controller.cc", - "line": 95 - }, - { - "file": "tmp/redpanda-dev/src/v/storage/backlog_controller.cc", - "line": 149 - } - ] - }, - "misses": { - "name": "misses", - "type": "counter", - "description": "The number of times a schema wasn't in the cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/record_schema_resolver.cc", - "line": 307 - } - ] - }, - "hits": { - "name": "hits", - "type": "counter", - "description": "The number of times a schema was in the cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/record_schema_resolver.cc", - "line": 314 - } - ] - }, - "translations_finished": { - "name": "translations_finished", - "type": "counter", - "description": "Number of finished translator executions", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", - "line": 49 - } - ] - }, - "files_created": { - "name": "files_created", - "type": "counter", - "description": "Number of created parquet files (not counting the DLQ table)", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", - "line": 58 - } - ] - }, - "parquet_rows_added": { - "name": "parquet_rows_added", - "type": "counter", - "description": "Number of rows in created parquet files (not ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", - "line": 68 - } - ] - }, - "parquet_bytes_added": { - "name": "parquet_bytes_added", - "type": "counter", - "description": "Number of bytes in created parquet files (not ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", - "line": 78 - } - ] - }, - "dlq_files_created": { - "name": "dlq_files_created", - "type": "counter", - "description": "Number of created parquet files for the DLQ table", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", - "line": 88 - } - ] - }, - "raw_bytes_processed": { - "name": "raw_bytes_processed", - "type": "counter", - "description": "Number of raw, potentially compressed bytes consumed for ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", - "line": 110 - } - ] - }, - "decompressed_bytes_processed": { - "name": "decompressed_bytes_processed", - "type": "counter", - "description": "Number of bytes post-decompression consumed for processing that ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", - "line": 125 - } - ] - }, - "invalid_records": { - "name": "invalid_records", - "type": "counter", - "description": "Number of invalid records handled by translation", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/datalake/translation/translation_probe.cc", - "line": 159 - } - ] - }, - "last_successful_bundle_timestamp_seconds": { - "name": "last_successful_bundle_timestamp_seconds", - "type": "gauge", - "description": "Timestamp of last successful debug bundle ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/debug_bundle/probe.cc", - "line": 34 - } - ] - }, - "last_failed_bundle_timestamp_seconds": { - "name": "last_failed_bundle_timestamp_seconds", - "type": "gauge", - "description": "Timestamp of last failed debug bundle ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/debug_bundle/probe.cc", - "line": 41 - } - ] - }, - "successful_generation_count": { - "name": "successful_generation_count", - "type": "counter", - "description": "Running count of successful debug bundle generations", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/debug_bundle/probe.cc", - "line": 48 - } - ] - }, - "failed_generation_count": { - "name": "failed_generation_count", - "type": "counter", - "description": "Running count of failed debug bundle generations", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/debug_bundle/probe.cc", - "line": 55 - } - ] - }, - "enterprise_license_expiry_sec": { - "name": "enterprise_license_expiry_sec", - "type": "gauge", - "description": "Number of seconds remaining until the ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/features/feature_table.cc", - "line": 278 - } - ] - }, - "total_puts": { - "name": "total_puts", - "type": "counter", - "description": "Number of completed PUT requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 41 - } - ] - }, - "total_gets": { - "name": "total_gets", - "type": "counter", - "description": "Number of completed GET requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 47 - } - ] - }, - "total_requests": { - "name": "total_requests", - "type": "counter", - "description": "Number of completed HTTP requests (includes PUT and GET)", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 53 - } - ] - }, - "active_puts": { - "name": "active_puts", - "type": "gauge", - "description": "Number of active PUT requests at the moment", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 60 - } - ] - }, - "active_gets": { - "name": "active_gets", - "type": "gauge", - "description": "Number of active GET requests at the moment", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 66 - } - ] - }, - "num_request_timeouts": { - "name": "num_request_timeouts", - "type": "counter", - "description": "Total number of catalog requests that could no longer be retried ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 99 - } - ] - }, - "num_oauth_token_requests": { - "name": "num_oauth_token_requests", - "type": "counter", - "description": "Total number of requests sent to the oauth_token endpoint", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 107 - } - ] - }, - "num_oauth_token_requests_failed": { - "name": "num_oauth_token_requests_failed", - "type": "counter", - "description": "Number of requests sent to the oauth_token endpoint that failed", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 114 - } - ] - }, - "num_create_namespace_requests": { - "name": "num_create_namespace_requests", - "type": "counter", - "description": "Total number of requests sent to the create_namespace endpoint", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 121 - } - ] - }, - "num_create_namespace_requests_failed": { - "name": "num_create_namespace_requests_failed", - "type": "counter", - "description": "Number of requests sent to the create_namespace ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 128 - } - ] - }, - "num_create_table_requests": { - "name": "num_create_table_requests", - "type": "counter", - "description": "Total number of requests sent to the create_table endpoint", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 135 - } - ] - }, - "num_create_table_requests_failed": { - "name": "num_create_table_requests_failed", - "type": "counter", - "description": "Number of requests sent to the create_table endpoint that failed", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 142 - } - ] - }, - "num_load_table_requests": { - "name": "num_load_table_requests", - "type": "counter", - "description": "Total number of requests sent to the load_table endpoint", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 149 - } - ] - }, - "num_load_table_requests_failed": { - "name": "num_load_table_requests_failed", - "type": "counter", - "description": "Number of requests sent to the load_table endpoint that failed", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 156 - } - ] - }, - "num_drop_table_requests": { - "name": "num_drop_table_requests", - "type": "counter", - "description": "Total number of requests sent to the drop_table endpoint", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 163 - } - ] - }, - "num_drop_table_requests_failed": { - "name": "num_drop_table_requests_failed", - "type": "counter", - "description": "Number of requests sent to the drop_table endpoint that failed", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 170 - } - ] - }, - "num_commit_table_update_requests": { - "name": "num_commit_table_update_requests", - "type": "counter", - "description": "Total number of requests sent to the ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 177 - } - ] - }, - "num_commit_table_update_requests_failed": { - "name": "num_commit_table_update_requests_failed", - "type": "counter", - "description": "Number of requests sent to the commit_table_update ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 184 - } - ] - }, - "num_get_config_requests": { - "name": "num_get_config_requests", - "type": "counter", - "description": "Total number of requests sent to the ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 191 - } - ] - }, - "num_get_config_requests_failed": { - "name": "num_get_config_requests_failed", - "type": "counter", - "description": "Number of requests sent to the config ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/iceberg/rest_client/client_probe.cc", - "line": 198 - } - ] - }, - "total_throttle": { - "name": "total_throttle", - "type": "counter", - "description": "Total datalake producer throttle time in milliseconds", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/datalake_throttle_manager.cc", - "line": 266 - } - ] - }, - "throttled_requests": { - "name": "throttled_requests", - "type": "counter", - "description": "Number of requests throttled", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/datalake_throttle_manager.cc", - "line": 271 - } - ] - }, - "delay_seconds_total": { - "name": "delay_seconds_total", - "type": "counter", - "description": "A running total of fetch delay set by the pid controller.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/fetch_pid_controller.cc", - "line": 151 - } - ] - }, - "error_total": { - "name": "error_total", - "type": "counter", - "description": "A running total of error in the fetch PID controller.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/fetch_pid_controller.cc", - "line": 156 - } - ] - }, - "mem_usage_bytes": { - "name": "mem_usage_bytes", - "type": "gauge", - "description": "Fetch sessions cache memory usage in bytes", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/fetch_session_cache.cc", - "line": 180 - } - ] - }, - "sessions_count": { - "name": "sessions_count", - "type": "gauge", - "description": "Total number of fetch sessions", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/fetch_session_cache.cc", - "line": 184 - } - ] - }, - "client_quota_throttle_time": { - "name": "client_quota_throttle_time", - "type": "histogram", - "description": "Client quota throttling delay per rule and ", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/quota_manager.cc", - "line": 69 - } - ] - }, - "client_quota_throughput": { - "name": "client_quota_throughput", - "type": "histogram", - "description": "Client quota throughput per rule and quota type", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/quota_manager.cc", - "line": 80 - } - ] - }, - "fetch_avail_mem_bytes": { - "name": "fetch_avail_mem_bytes", - "type": "counter", - "description": "{}: Memory available for fetch request processing", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/server.cc", - "line": 226 - } - ] - }, - "traffic_intake": { - "name": "traffic_intake", - "type": "counter", - "description": "Amount of Kafka traffic received from the clients ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/snc_quota_manager.cc", - "line": 91 - } - ] - }, - "traffic_egress": { - "name": "traffic_egress", - "type": "counter", - "description": "Amount of Kafka traffic published to the clients ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/snc_quota_manager.cc", - "line": 97 - } - ] - }, - "throttle_time": { - "name": "throttle_time", - "type": "histogram", - "description": "Throttle time histogram (in seconds)", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/snc_quota_manager.cc", - "line": 103 - } - ] - }, - "requests_errored_total": { - "name": "requests_errored_total", - "type": "counter", - "description": "Number of kafka requests errored", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", - "line": 78 - } - ] - }, - "requests_in_progress_total": { - "name": "requests_in_progress_total", - "type": "counter", - "description": "A running total of kafka requests in progress", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", - "line": 83 - } - ] - }, - "received_bytes_total": { - "name": "received_bytes_total", - "type": "counter", - "description": "Number of bytes received from kafka requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", - "line": 88 - } - ] - }, - "sent_bytes_total": { - "name": "sent_bytes_total", - "type": "counter", - "description": "Number of bytes sent in kafka replies", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", - "line": 93 - } - ] - }, - "latency_microseconds": { - "name": "latency_microseconds", - "type": "histogram", - "description": "Latency histogram of kafka requests", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", - "line": 98 - } - ] - }, - "requests_completed_total": { - "name": "requests_completed_total", - "type": "counter", - "description": "Number of kafka requests completed", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", - "line": 112 - } - ] - }, - "latency_seconds": { - "name": "latency_seconds", - "type": "histogram", - "description": "Latency histogram of kafka requests", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/kafka/server/handlers/handler_probe.cc", - "line": 137 - }, - { - "file": "tmp/redpanda-dev/src/v/net/server.cc", - "line": 362 - }, - { - "file": "tmp/redpanda-dev/src/v/security/oidc_service.cc", - "line": 94 - } - ] - }, - "Host diskstat {}": { - "name": "Host diskstat {}", - "type": "gauge", - "description": "", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", - "line": 118 - } - ] - }, - "packets_received": { - "name": "packets_received", - "type": "counter", - "description": "Host IP packets received", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", - "line": 156 - } - ] - }, - "packets_sent": { - "name": "packets_sent", - "type": "counter", - "description": "Host IP packets sent", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", - "line": 163 - } - ] - }, - "tcp_established": { - "name": "tcp_established", - "type": "gauge", - "description": "Host TCP established connections", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/metrics/host_metrics_watcher.cc", - "line": 170 - } - ] - }, - "active_connections": { - "name": "active_connections", - "type": "gauge", - "description": "{}: Currently active connections", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 44 - } - ] - }, - "connects": { - "name": "connects", - "type": "counter", - "description": "{}: Number of accepted connections", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 49 - } - ] - }, - "connection_close_errors": { - "name": "connection_close_errors", - "type": "counter", - "description": "{}: Number of errors when shutting down the connection", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 54 - } - ] - }, - "connections_rejected": { - "name": "connections_rejected", - "type": "counter", - "description": "{}: Number of connection attempts rejected for hitting open ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 59 - } - ] - }, - "connections_rejected_rate_limit": { - "name": "connections_rejected_rate_limit", - "type": "counter", - "description": "{}: Number of connection attempts rejected for hitting ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 66 - } - ] - }, - "requests_completed": { - "name": "requests_completed", - "type": "counter", - "description": "{}: Number of successful requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 73 - } - ] - }, - "received_bytes": { - "name": "received_bytes", - "type": "counter", - "description": "{}: Number of bytes received from the clients in valid requests", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 78 - } - ] - }, - "sent_bytes": { - "name": "sent_bytes", - "type": "counter", - "description": "{}: Number of bytes sent to clients", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 84 - } - ] - }, - "method_not_found_errors": { - "name": "method_not_found_errors", - "type": "counter", - "description": "{}: Number of requests with not available RPC method", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 89 - } - ] - }, - "corrupted_headers": { - "name": "corrupted_headers", - "type": "counter", - "description": "{}: Number of requests with corrupted headers", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 94 - }, - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 546 - } - ] - }, - "service_errors": { - "name": "service_errors", - "type": "counter", - "description": "{}: Number of service errors", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 99 - } - ] - }, - "requests_blocked_memory": { - "name": "requests_blocked_memory", - "type": "counter", - "description": "{}: Number of requests blocked in memory backpressure", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 103 - }, - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 570 - } - ] - }, - "requests_pending": { - "name": "requests_pending", - "type": "gauge", - "description": "{}: Number of requests pending in the queue", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 108 - }, - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 502 - } - ] - }, - "connections_wait_rate": { - "name": "connections_wait_rate", - "type": "counter", - "description": "{}: Number of connections are blocked by connection rate", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 113 - } - ] - }, - "request_errors_total": { - "name": "request_errors_total", - "type": "counter", - "description": "Number of rpc errors", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 137 - }, - { - "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", - "line": 84 - } - ] - }, - "connection_errors": { - "name": "connection_errors", - "type": "counter", - "description": "Number of connection errors", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 209 - } - ] - }, - "truststore_expires_at_timestamp_seconds": { - "name": "truststore_expires_at_timestamp_seconds", - "type": "gauge", - "description": "Expiry time of the shortest-lived CA in the truststore", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 358 - } - ] - }, - "certificate_expires_at_timestamp_seconds": { - "name": "certificate_expires_at_timestamp_seconds", - "type": "gauge", - "description": "Expiry time of the server certificate (seconds since epoch)", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 369 - } - ] - }, - "certificate_serial": { - "name": "certificate_serial", - "type": "gauge", - "description": "Least significant four bytes of the server ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 379 - } - ] - }, - "loaded_at_timestamp_seconds": { - "name": "loaded_at_timestamp_seconds", - "type": "gauge", - "description": "Load time of the server certificate (seconds since epoch).", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 387 - } - ] - }, - "certificate_valid": { - "name": "certificate_valid", - "type": "gauge", - "description": "The value is one if the certificate is valid with ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 395 - } - ] - }, - "trust_file_crc32c": { - "name": "trust_file_crc32c", - "type": "gauge", - "description": "crc32c calculated from the contents of the trust ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/probes.cc", - "line": 403 - } - ] - }, - "max_service_mem_bytes": { - "name": "max_service_mem_bytes", - "type": "counter", - "description": "{}: Maximum memory allowed for RPC", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/server.cc", - "line": 333 - } - ] - }, - "consumed_mem_bytes": { - "name": "consumed_mem_bytes", - "type": "counter", - "description": "{}: Memory consumed by request processing", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/server.cc", - "line": 338 - } - ] - }, - "dispatch_handler_latency": { - "name": "dispatch_handler_latency", - "type": "histogram", - "description": "{}: Latency ", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/net/server.cc", - "line": 343 - } - ] - }, - "request_latency": { - "name": "request_latency", - "type": "histogram", - "description": "Request latency", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", - "line": 61 - } - ] - }, - "request_latency_seconds": { - "name": "request_latency_seconds", - "type": "histogram", - "description": "Internal latency of request for {}", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", - "line": 72 - } - ] - }, - "inflight_requests_usage_ratio": { - "name": "inflight_requests_usage_ratio", - "type": "gauge", - "description": "Usage ratio of in-flight requests in the {}", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", - "line": 165 - } - ] - }, - "inflight_requests_memory_usage_ratio": { - "name": "inflight_requests_memory_usage_ratio", - "type": "gauge", - "description": "Memory usage ratio of in-flight requests in the {}", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", - "line": 174 - } - ] - }, - "queued_requests_memory_blocked": { - "name": "queued_requests_memory_blocked", - "type": "gauge", - "description": "Number of requests queued in {}, due to memory limitations", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/pandaproxy/probe.cc", - "line": 184 - } - ] - }, - "inflight_requests": { - "name": "inflight_requests", - "type": "gauge", - "description": "Number of append entries requests that were sent to ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/buffered_protocol.cc", - "line": 378 - } - ] - }, - "buffered_bytes": { - "name": "buffered_bytes", - "type": "gauge", - "description": "Total size of append entries requests in the queue", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/buffered_protocol.cc", - "line": 384 - } - ] - }, - "buffered_requests": { - "name": "buffered_requests", - "type": "gauge", - "description": "Total number of append entries requests in the queue", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/buffered_protocol.cc", - "line": 389 - } - ] - }, - "leader_for": { - "name": "leader_for", - "type": "gauge", - "description": "Number of groups for which node is a leader", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/consensus.cc", - "line": 180 - } - ] - }, - "configuration_change_in_progress": { - "name": "configuration_change_in_progress", - "type": "gauge", - "description": "Indicates if current raft group configuration is in ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/consensus.cc", - "line": 185 - } - ] - }, - "partition_movement_available_bandwidth": { - "name": "partition_movement_available_bandwidth", - "type": "gauge", - "description": "Bandwidth available for partition movement. bytes/sec", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/coordinated_recovery_throttle.cc", - "line": 80 - } - ] - }, - "partition_movement_assigned_bandwidth": { - "name": "partition_movement_assigned_bandwidth", - "type": "gauge", - "description": "Bandwidth assigned for partition movement in last ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/coordinated_recovery_throttle.cc", - "line": 86 - } - ] - }, - "partition_movement_consumed_bandwidth": { - "name": "partition_movement_consumed_bandwidth", - "type": "gauge", - "description": "Bandwidth consumed for partition movement. bytes/sec", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/coordinated_recovery_throttle.cc", - "line": 106 - } - ] - }, - "group_count": { - "name": "group_count", - "type": "gauge", - "description": "Number of raft groups", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/group_manager.cc", - "line": 218 - } - ] - }, - "learners_gap_bytes": { - "name": "learners_gap_bytes", - "type": "gauge", - "description": "Total numbers of bytes that must be delivered to learners", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/group_manager.cc", - "line": 222 - } - ] - }, - "leadership_changes": { - "name": "leadership_changes", - "type": "counter", - "description": "Number of won leader elections across all partitions ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 46 - } - ] - }, - "received_vote_requests": { - "name": "received_vote_requests", - "type": "counter", - "description": "Number of vote requests received", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 62 - } - ] - }, - "received_append_requests": { - "name": "received_append_requests", - "type": "counter", - "description": "Number of append requests received", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 67 - } - ] - }, - "sent_vote_requests": { - "name": "sent_vote_requests", - "type": "counter", - "description": "Number of vote requests sent", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 72 - } - ] - }, - "replicate_ack_all_requests": { - "name": "replicate_ack_all_requests", - "type": "counter", - "description": "Number of replicate requests with quorum ack ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 77 - } - ] - }, - "replicate_ack_all_requests_no_flush": { - "name": "replicate_ack_all_requests_no_flush", - "type": "counter", - "description": "Number of replicate requests with quorum ack ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 83 - } - ] - }, - "replicate_ack_leader_requests": { - "name": "replicate_ack_leader_requests", - "type": "counter", - "description": "Number of replicate requests with leader ack consistency", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 89 - } - ] - }, - "replicate_ack_none_requests": { - "name": "replicate_ack_none_requests", - "type": "counter", - "description": "Number of replicate requests with no ack consistency", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 95 - } - ] - }, - "done_replicate_requests": { - "name": "done_replicate_requests", - "type": "counter", - "description": "Number of finished replicate requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 101 - } - ] - }, - "log_flushes": { - "name": "log_flushes", - "type": "counter", - "description": "Number of log flushes", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 106 - } - ] - }, - "log_truncations": { - "name": "log_truncations", - "type": "counter", - "description": "Number of log truncations", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 111 - } - ] - }, - "replicate_request_errors": { - "name": "replicate_request_errors", - "type": "counter", - "description": "Number of failed replicate requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 121 - } - ] - }, - "heartbeat_requests_errors": { - "name": "heartbeat_requests_errors", - "type": "counter", - "description": "Number of failed heartbeat requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 126 - } - ] - }, - "recovery_requests_errors": { - "name": "recovery_requests_errors", - "type": "counter", - "description": "Number of failed recovery requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 131 - } - ] - }, - "recovery_requests": { - "name": "recovery_requests", - "type": "counter", - "description": "Number of recovery requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 136 - } - ] - }, - "group_configuration_updates": { - "name": "group_configuration_updates", - "type": "counter", - "description": "Number of raft group configuration updates", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 141 - } - ] - }, - "replicate_batch_flush_requests": { - "name": "replicate_batch_flush_requests", - "type": "counter", - "description": "Number of replicate batch flushes", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 146 - } - ] - }, - "lightweight_heartbeat_requests": { - "name": "lightweight_heartbeat_requests", - "type": "counter", - "description": "Number of lightweight heartbeats sent by the leader", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 151 - } - ] - }, - "full_heartbeat_requests": { - "name": "full_heartbeat_requests", - "type": "counter", - "description": "Number of full heartbeats sent by the leader", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 157 - } - ] - }, - "offset_translator_inconsistency_errors": { - "name": "offset_translator_inconsistency_errors", - "type": "counter", - "description": "Number of append entries requests that failed the ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 162 - } - ] - }, - "append_entries_buffer_flushes": { - "name": "append_entries_buffer_flushes", - "type": "counter", - "description": "Number of append entries buffer flushes", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/probe.cc", - "line": 168 - } - ] - }, - "partitions_to_recover": { - "name": "partitions_to_recover", - "type": "gauge", - "description": "Number of partition replicas that have to ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/recovery_scheduler.cc", - "line": 372 - } - ] - }, - "partitions_active": { - "name": "partitions_active", - "type": "gauge", - "description": "Number of partition replicas are currently ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/recovery_scheduler.cc", - "line": 379 - } - ] - }, - "offsets_pending": { - "name": "offsets_pending", - "type": "gauge", - "description": "Sum of offsets that partitions on this node ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/raft/recovery_scheduler.cc", - "line": 386 - } - ] - }, - "uptime_seconds_total": { - "name": "uptime_seconds_total", - "type": "gauge", - "description": "Redpanda uptime in seconds", - "labels": [ - "git_revision", - "git_version" - ], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/redpanda/application.cc", - "line": 731 - } - ] - }, - "build": { - "name": "build", - "type": "gauge", - "description": "Redpanda build information", - "labels": [ - "git_revision", - "git_version" - ], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/redpanda/application.cc", - "line": 739 - } - ] - }, - "fips_mode": { - "name": "fips_mode", - "type": "gauge", - "description": "Identifies whether or not Redpanda is ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/redpanda/application.cc", - "line": 745 - } - ] - }, - "busy_seconds_total": { - "name": "busy_seconds_total", - "type": "gauge", - "description": "Total CPU busy time in seconds", - "labels": [ - "git_", - "git_version" - ], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/redpanda/application.cc", - "line": 761 - } - ] - }, - "uptime": { - "name": "uptime", - "type": "gauge", - "description": "Redpanda uptime in milliseconds", - "labels": [ - "git_revision", - "git_version" - ], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/redpanda/application.cc", - "line": 792 - } - ] - }, - "target_disk_size_bytes": { - "name": "target_disk_size_bytes", - "type": "gauge", - "description": "Target maximum number of stored bytes.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 730 - } - ] - }, - "disk_usage_bytes": { - "name": "disk_usage_bytes", - "type": "gauge", - "description": "Total amount of disk usage under control of space management.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 735 - } - ] - }, - "datalake_disk_usage_bytes": { - "name": "datalake_disk_usage_bytes", - "type": "gauge", - "description": "Total amount of disk usage by datalake.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 741 - } - ] - }, - "retention_reclaimable_bytes": { - "name": "retention_reclaimable_bytes", - "type": "gauge", - "description": "Total amount of reclaimable data through standard ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 746 - } - ] - }, - "available_reclaimable_bytes": { - "name": "available_reclaimable_bytes", - "type": "gauge", - "description": "Total amount of available reclaimable data by space ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 752 - } - ] - }, - "local_retention_reclaimable_bytes": { - "name": "local_retention_reclaimable_bytes", - "type": "gauge", - "description": "Total amount of reclaimable data above the local ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 758 - } - ] - }, - "target_excess_bytes": { - "name": "target_excess_bytes", - "type": "gauge", - "description": "Amount of data usage that exceeds target threshold.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 765 - } - ] - }, - "reclaim_local_bytes": { - "name": "reclaim_local_bytes", - "type": "gauge", - "description": "Estimated amount of data above local retention to be ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 770 - } - ] - }, - "reclaim_low_non_hinted_bytes": { - "name": "reclaim_low_non_hinted_bytes", - "type": "gauge", - "description": "Estimated amount of data above the non-hinted low-space ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 776 - } - ] - }, - "reclaim_low_hinted_bytes": { - "name": "reclaim_low_hinted_bytes", - "type": "gauge", - "description": "Estimated amount of data above the hinted low-space ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 782 - } - ] - }, - "reclaim_active_segment_bytes": { - "name": "reclaim_active_segment_bytes", - "type": "gauge", - "description": "Estimated amount of data above the active segment to be ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 788 - } - ] - }, - "reclaim_estimate_bytes": { - "name": "reclaim_estimate_bytes", - "type": "gauge", - "description": "Estimated amount of data to be reclaimed by space ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/resource_mgmt/storage.cc", - "line": 794 - } - ] - }, - "requests": { - "name": "requests", - "type": "counter", - "description": "Number of requests", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 495 - } - ] - }, - "request_errors": { - "name": "request_errors", - "type": "counter", - "description": "Number or requests errors", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 509 - } - ] - }, - "request_timeouts": { - "name": "request_timeouts", - "type": "counter", - "description": "Number or requests timeouts", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 516 - } - ] - }, - "out_bytes": { - "name": "out_bytes", - "type": "counter", - "description": "Total number of bytes sent (including headers)", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 524 - } - ] - }, - "in_bytes": { - "name": "in_bytes", - "type": "counter", - "description": "Total number of bytes received", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 531 - } - ] - }, - "read_dispatch_errors": { - "name": "read_dispatch_errors", - "type": "counter", - "description": "Number of errors while dispatching responses", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 538 - } - ] - }, - "server_correlation_errors": { - "name": "server_correlation_errors", - "type": "counter", - "description": "Number of responses with wrong correlation id", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 554 - } - ] - }, - "client_correlation_errors": { - "name": "client_correlation_errors", - "type": "counter", - "description": "Number of errors in client correlation id", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/rpc/transport.cc", - "line": 562 - } - ] - }, - "result": { - "name": "result", - "type": "counter", - "description": "Total number of authorization results by type", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/security/authorizer.cc", - "line": 55 - } - ] - }, - "last_event_timestamp_seconds": { - "name": "last_event_timestamp_seconds", - "type": "counter", - "description": "Timestamp of last successful publish on the ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/security/audit/probes.cc", - "line": 32 - } - ] - }, - "buffer_usage_ratio": { - "name": "buffer_usage_ratio", - "type": "gauge", - "description": "Audit event buffer usage ratio.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/security/audit/probes.cc", - "line": 54 - }, - { - "file": "tmp/redpanda-dev/src/v/transform/logging/probes.cc", - "line": 80 - } - ] - }, - "error": { - "name": "error", - "type": "gauge", - "description": "current controller error, i.e difference between ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/backlog_controller.cc", - "line": 140 - } - ] - }, - "shares": { - "name": "shares", - "type": "gauge", - "description": "controller output, i.e. number of shares", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/backlog_controller.cc", - "line": 145 - } - ] - }, - "total_size_bytes": { - "name": "total_size_bytes", - "type": "gauge", - "description": "Total size of all segment appender chunks in any ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/chunk_cache.cc", - "line": 56 - } - ] - }, - "available_size_bytes": { - "name": "available_size_bytes", - "type": "gauge", - "description": "Total size of all free segment appender chunks in ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/chunk_cache.cc", - "line": 61 - } - ] - }, - "wait_count": { - "name": "wait_count", - "type": "counter", - "description": "Count of how many times we had to wait for a chunk ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/chunk_cache.cc", - "line": 66 - } - ] - }, - "segments_rolled": { - "name": "segments_rolled", - "type": "counter", - "description": "Number of segments rolled", - "labels": [], - "constructor": "make_total_operations", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/kvstore.cc", - "line": 70 - } - ] - }, - "entries_fetched": { - "name": "entries_fetched", - "type": "counter", - "description": "Number of entries fetched", - "labels": [], - "constructor": "make_total_operations", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/kvstore.cc", - "line": 74 - } - ] - }, - "entries_written": { - "name": "entries_written", - "type": "counter", - "description": "Number of entries written", - "labels": [], - "constructor": "make_total_operations", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/kvstore.cc", - "line": 78 - } - ] - }, - "entries_removed": { - "name": "entries_removed", - "type": "counter", - "description": "Number of entries removaled", - "labels": [], - "constructor": "make_total_operations", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/kvstore.cc", - "line": 82 - } - ] - }, - "cached_bytes": { - "name": "cached_bytes", - "type": "gauge", - "description": "Size of the database in memory", - "labels": [], - "constructor": "make_current_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/kvstore.cc", - "line": 86 - } - ] - }, - "logs": { - "name": "logs", - "type": "gauge", - "description": "Number of logs managed", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/log_manager_probe.cc", - "line": 31 - } - ] - }, - "urgent_gc_runs": { - "name": "urgent_gc_runs", - "type": "counter", - "description": "Number of urgent GC runs", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/log_manager_probe.cc", - "line": 35 - } - ] - }, - "housekeeping_log_processed": { - "name": "housekeeping_log_processed", - "type": "counter", - "description": "Number of logs processed by housekeeping", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/log_manager_probe.cc", - "line": 39 - } - ] - }, - "total_bytes": { - "name": "total_bytes", - "type": "gauge", - "description": "Total size of attached storage, in bytes.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 50 - } - ] - }, - "free_bytes": { - "name": "free_bytes", - "type": "gauge", - "description": "Disk storage bytes free.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 55 - } - ] - }, - "free_space_alert": { - "name": "free_space_alert", - "type": "gauge", - "description": "Status of low storage space alert. 0-OK, 1-Low Space 2-Degraded", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 60 - } - ] - }, - "written_bytes": { - "name": "written_bytes", - "type": "counter", - "description": "Total number of bytes written", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 113 - } - ] - }, - "batches_written": { - "name": "batches_written", - "type": "counter", - "description": "Total number of batches written", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 118 - } - ] - }, - "cached_read_bytes": { - "name": "cached_read_bytes", - "type": "counter", - "description": "Total number of cached bytes read", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 128 - } - ] - }, - "batches_read": { - "name": "batches_read", - "type": "counter", - "description": "Total number of batches read", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 133 - } - ] - }, - "cached_batches_read": { - "name": "cached_batches_read", - "type": "counter", - "description": "Total number of cached batches read", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 138 - } - ] - }, - "log_segments_created": { - "name": "log_segments_created", - "type": "counter", - "description": "Total number of local log segments created since node startup", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 143 - } - ] - }, - "log_segments_removed": { - "name": "log_segments_removed", - "type": "counter", - "description": "Total number of local log segments removed since node startup", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 149 - } - ] - }, - "log_segments_active": { - "name": "log_segments_active", - "type": "counter", - "description": "Current number of local log segments", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 155 - } - ] - }, - "batch_parse_errors": { - "name": "batch_parse_errors", - "type": "counter", - "description": "Number of batch parsing (reading) errors", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 160 - } - ] - }, - "batch_write_errors": { - "name": "batch_write_errors", - "type": "counter", - "description": "Number of batch write errors", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 165 - } - ] - }, - "corrupted_compaction_indices": { - "name": "corrupted_compaction_indices", - "type": "counter", - "description": "Number of times we had to re-construct the ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 170 - } - ] - }, - "compacted_segment": { - "name": "compacted_segment", - "type": "counter", - "description": "Number of compacted segments", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 176 - } - ] - }, - "partition_size": { - "name": "partition_size", - "type": "gauge", - "description": "Current size of partition in bytes", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 181 - } - ] - }, - "bytes_prefix_truncated": { - "name": "bytes_prefix_truncated", - "type": "counter", - "description": "Number of bytes removed by prefix truncation.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 186 - } - ] - }, - "compaction_removed_bytes": { - "name": "compaction_removed_bytes", - "type": "counter", - "description": "Number of bytes removed by a compaction operation", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 191 - } - ] - }, - "tombstones_removed": { - "name": "tombstones_removed", - "type": "counter", - "description": "Number of tombstone records removed by compaction ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 196 - } - ] - }, - "cleanly_compacted_segment": { - "name": "cleanly_compacted_segment", - "type": "counter", - "description": "Number of segments cleanly compacted (i.e, had their ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 202 - } - ] - }, - "segments_marked_tombstone_free": { - "name": "segments_marked_tombstone_free", - "type": "counter", - "description": "Number of segments that have been verified through ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 210 - } - ] - }, - "complete_sliding_window_rounds": { - "name": "complete_sliding_window_rounds", - "type": "counter", - "description": "Number of rounds of sliding window compaction that ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 216 - } - ] - }, - "chunked_compaction_runs": { - "name": "chunked_compaction_runs", - "type": "counter", - "description": "Number of times chunked compaction was ran. This metric also ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 222 - } - ] - }, - "dirty_segment_bytes": { - "name": "dirty_segment_bytes", - "type": "gauge", - "description": "Number of bytes within dirty segments of the log", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 230 - } - ] - }, - "closed_segment_bytes": { - "name": "closed_segment_bytes", - "type": "gauge", - "description": "Number of bytes within closed segments of the log", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 235 - } - ] - }, - "adjacent_segments_compacted": { - "name": "adjacent_segments_compacted", - "type": "counter", - "description": "Number of segments that have been compacted away ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 240 - } - ] - }, - "compaction_ratio": { - "name": "compaction_ratio", - "type": "counter", - "description": "Average segment compaction ratio", - "labels": [], - "constructor": "make_total_bytes", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 255 - } - ] - }, - "readers_added": { - "name": "readers_added", - "type": "counter", - "description": "Number of readers added to cache", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 291 - } - ] - }, - "readers_evicted": { - "name": "readers_evicted", - "type": "counter", - "description": "Number of readers evicted from cache", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 296 - } - ] - }, - "cache_hits": { - "name": "cache_hits", - "type": "counter", - "description": "Reader cache hits", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 301 - } - ] - }, - "cache_misses": { - "name": "cache_misses", - "type": "counter", - "description": "Reader cache misses", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/storage/probe.cc", - "line": 306 - } - ] - }, - "failures": { - "name": "failures", - "type": "counter", - "description": "The number of transform failures", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/transform/probe.cc", - "line": 38 - } - ] - }, - "lag": { - "name": "lag", - "type": "gauge", - "description": "The number of pending records on the input topic that have ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/transform/probe.cc", - "line": 55 - } - ] - }, - "write_bytes": { - "name": "write_bytes", - "type": "counter", - "description": "The number of bytes output by the transform", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/transform/probe.cc", - "line": 64 - } - ] - }, - "state": { - "name": "state", - "type": "gauge", - "description": "The number of transforms in a specific state", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/transform/probe.cc", - "line": 79 - } - ] - }, - "events_total": { - "name": "events_total", - "type": "counter", - "description": "Running count of transform log events", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/transform/logging/probes.cc", - "line": 32 - } - ] - }, - "events_dropped_total": { - "name": "events_dropped_total", - "type": "counter", - "description": "Running count of dropped transform log events", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/transform/logging/probes.cc", - "line": 37 - } - ] - }, - "write_errors_total": { - "name": "write_errors_total", - "type": "counter", - "description": "Running count of errors while writing to the ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/transform/logging/probes.cc", - "line": 84 - } - ] - }, - "cpu_seconds_total": { - "name": "cpu_seconds_total", - "type": "counter", - "description": "Total CPU time (in seconds) spent inside a WebAssembly ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/wasm/engine_probe.cc", - "line": 38 - } - ] - }, - "memory_usage": { - "name": "memory_usage", - "type": "gauge", - "description": "Amount of memory usage for a WebAssembly function", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/wasm/engine_probe.cc", - "line": 46 - } - ] - }, - "max_memory": { - "name": "max_memory", - "type": "gauge", - "description": "Max amount of memory for a WebAssembly function", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/wasm/engine_probe.cc", - "line": 52 - } - ] - }, - "latency_sec": { - "name": "latency_sec", - "type": "histogram", - "description": "A histogram of the latency in seconds of ", - "labels": [], - "constructor": "make_histogram", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/wasm/transform_probe.cc", - "line": 33 - } - ] - }, - "errors": { - "name": "errors", - "type": "counter", - "description": "Data transform invocation errors", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/wasm/transform_probe.cc", - "line": 40 - } - ] - }, - "executable_memory_usage": { - "name": "executable_memory_usage", - "type": "gauge", - "description": "The amount of executable memory used for ", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/wasm/wasmtime.cc", - "line": 1433 - } - ] - } - }, - "statistics": { - "total_metrics": 378, - "by_type": { - "counter": 223, - "gauge": 141, - "histogram": 14 - }, - "by_constructor": { - "make_counter": 201, - "make_gauge": 140, - "make_total_bytes": 18, - "make_histogram": 14, - "make_total_operations": 4, - "make_current_bytes": 1 - }, - "with_description": 376, - "with_labels": 4 - } -} \ No newline at end of file diff --git a/tools/metrics-extractor/metrics_extractor.py b/tools/metrics-extractor/metrics_extractor.py index 8e88bed..f16a0cd 100644 --- a/tools/metrics-extractor/metrics_extractor.py +++ b/tools/metrics-extractor/metrics_extractor.py @@ -18,7 +18,7 @@ def validate_paths(options): - path = options.path + path = options.redpanda_repo if not os.path.exists(path): logger.error(f'Path does not exist: "{path}".') @@ -27,7 +27,7 @@ def validate_paths(options): def get_cpp_files(options): """Get all C++ source files from the path""" - path = Path(options.path) + path = Path(options.redpanda_repo) # If the path is a file, return it directly if path.is_file() and path.suffix in ['.cc', '.cpp', '.cxx', '.h', '.hpp']: @@ -63,18 +63,19 @@ def parse_args(): description="Extract Redpanda metrics from C++ source code using tree-sitter" ) parser.add_argument( - "path", + "--redpanda-repo", + "-r", + required=True, help="Path to the Redpanda source code directory" ) parser.add_argument( "--recursive", - "-r", action="store_true", - help="Search for C++ files recursively" + default=True, + help="Search for C++ files recursively (default: True)" ) parser.add_argument( - "--output", - "-o", + "--json-output", default="metrics.json", help="Output JSON file (default: metrics.json)" ) @@ -421,7 +422,7 @@ def main(): print(f"Internal metrics: {internal_count}") print(f"External metrics: {external_count}") - with open(args.output, 'w') as f: + with open(args.json_output, 'w') as f: json.dump(metrics_bag.to_dict(), f, indent=2) # Output AsciiDoc if requested @@ -436,7 +437,7 @@ def main(): generate_asciidoc_by_type(metrics_bag, args.asciidoc, None) # Only show summary messages, not duplicate file outputs - print(f"πŸ“„ JSON output: {args.output}") + print(f"πŸ“„ JSON output: {args.json_output}") if args.internal_asciidoc: print(f"πŸ“„ Internal metrics: {args.internal_asciidoc}") if args.external_asciidoc: diff --git a/tools/metrics-extractor/sample.json b/tools/metrics-extractor/sample.json deleted file mode 100644 index a61a895..0000000 --- a/tools/metrics-extractor/sample.json +++ /dev/null @@ -1,251 +0,0 @@ -{ - "metrics": { - "puts": { - "name": "puts", - "type": "counter", - "description": "Total number of files put into cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 27 - } - ] - }, - "gets": { - "name": "gets", - "type": "counter", - "description": "Total number of cache get requests.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 31 - } - ] - }, - "cached_gets": { - "name": "cached_gets", - "type": "counter", - "description": "Total number of get requests that are already in cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 35 - } - ] - }, - "size_bytes": { - "name": "size_bytes", - "type": "gauge", - "description": "Current cache size in bytes.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 40 - } - ] - }, - "files": { - "name": "files", - "type": "gauge", - "description": "Current number of files in cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 44 - } - ] - }, - "in_progress_files": { - "name": "in_progress_files", - "type": "gauge", - "description": "Current number of files that are being put to cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 48 - } - ] - }, - "hwm_size_bytes": { - "name": "hwm_size_bytes", - "type": "gauge", - "description": "High watermark of sum of size of cached objects.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 69 - } - ] - }, - "hwm_files": { - "name": "hwm_files", - "type": "gauge", - "description": "High watermark of number of objects in cache.", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 80 - } - ] - }, - "tracker_syncs": { - "name": "tracker_syncs", - "type": "counter", - "description": "Number of times the access tracker was updated ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 86 - } - ] - }, - "tracker_size": { - "name": "tracker_size", - "type": "gauge", - "description": "Number of entries in cache access tracker", - "labels": [], - "constructor": "make_gauge", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 93 - } - ] - }, - "fast_trims": { - "name": "fast_trims", - "type": "counter", - "description": "Number of times we have trimmed the cache ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 103 - } - ] - }, - "exhaustive_trims": { - "name": "exhaustive_trims", - "type": "counter", - "description": "Number of times we couldn't free enough space with a fast ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 109 - } - ] - }, - "carryover_trims": { - "name": "carryover_trims", - "type": "counter", - "description": "Number of times we invoked carryover trim.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 116 - } - ] - }, - "failed_trims": { - "name": "failed_trims", - "type": "counter", - "description": "Number of times could not free the expected amount of ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 121 - } - ] - }, - "in_mem_trims": { - "name": "in_mem_trims", - "type": "counter", - "description": "Number of times we trimmed the cache using ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 128 - } - ] - }, - "put": { - "name": "put", - "type": "counter", - "description": "Number of objects written into cache.", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 141 - } - ] - }, - "hit": { - "name": "hit", - "type": "counter", - "description": "Number of get requests for objects that are ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 146 - } - ] - }, - "miss": { - "name": "miss", - "type": "counter", - "description": "Number of failed get requests because of ", - "labels": [], - "constructor": "make_counter", - "files": [ - { - "file": "tmp/redpanda-dev/src/v/cloud_storage/cache_probe.cc", - "line": 152 - } - ] - } - }, - "statistics": { - "total_metrics": 18, - "by_type": { - "counter": 12, - "gauge": 6 - }, - "by_constructor": { - "make_counter": 12, - "make_gauge": 6 - }, - "with_description": 18, - "with_labels": 0 - } -} \ No newline at end of file diff --git a/tools/metrics-extractor/test_filtered.json b/tools/metrics-extractor/test_filtered.json deleted file mode 100644 index d0f6b48..0000000 --- a/tools/metrics-extractor/test_filtered.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "metrics": {}, - "statistics": { - "total_metrics": 0, - "by_type": {}, - "by_constructor": {}, - "with_description": 0, - "with_labels": 0 - } -} \ No newline at end of file From f649efe7c8313b999199b0993329fe0f6acb848e Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Tue, 22 Jul 2025 19:43:57 -0300 Subject: [PATCH 18/21] additional fixes --- tools/metrics-extractor/Makefile | 7 +++---- tools/metrics-extractor/metrics_parser.py | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/metrics-extractor/Makefile b/tools/metrics-extractor/Makefile index 26f17c8..982cc4c 100644 --- a/tools/metrics-extractor/Makefile +++ b/tools/metrics-extractor/Makefile @@ -80,12 +80,11 @@ extract-metrics: @echo "Extracting metrics from Redpanda source code..." @mkdir -p $(OUTPUT_DIR) $(PYTHON) metrics_extractor.py \ - --recursive \ - --output $(OUTPUT_DIR)/metrics.json \ + --redpanda-repo $(REDPANDA_DIR) \ + --json-output $(OUTPUT_DIR)/metrics.json \ --internal-asciidoc $(OUTPUT_DIR)/internal_metrics_reference.adoc \ --external-asciidoc $(OUTPUT_DIR)/public_metrics_reference.adoc \ - --verbose \ - $(REDPANDA_DIR)/src + --verbose generate-comparison: @echo "Generating metrics comparison..." diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py index 08b98bf..da1d850 100644 --- a/tools/metrics-extractor/metrics_parser.py +++ b/tools/metrics-extractor/metrics_parser.py @@ -406,6 +406,11 @@ def collect_string_info(node): # Final fallback: just use the second string literal description = string_literals[1]['text'] + # Filter out descriptions with unresolved format placeholders + if description and '{}' in description: + logger.debug(f"Filtering out description with unresolved placeholders: '{description}'") + description = "" + return metric_name, description From ab66342930bed34a40de9bb3ea2b7ed0f41c4140 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 23 Jul 2025 17:15:15 -0300 Subject: [PATCH 19/21] try to solve group_names and seastar related metrics --- tools/metrics-extractor/metrics_parser.py | 760 +++++++++++++++++++++- 1 file changed, 729 insertions(+), 31 deletions(-) diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py index da1d850..d5f49ca 100644 --- a/tools/metrics-extractor/metrics_parser.py +++ b/tools/metrics-extractor/metrics_parser.py @@ -128,7 +128,72 @@ def extract_labels_from_code(code_context): return sorted(list(labels)) -def find_group_name_and_type_from_ast(metric_call_expr_node): +def determine_metric_type_from_variable(start_node, variable_name, file_path): + """ + Determine if a metrics variable is internal or external by searching for its declaration. + Looks for patterns like: + - metrics::public_metric_groups _service_metrics; (external) + - ss::metrics::metric_groups _metrics; (internal) + """ + + # Go to the root of the file to search for declarations + root_node = start_node + while root_node.parent: + root_node = root_node.parent + + def search_for_variable_declaration(node): + if node.type == 'declaration': + declaration_text = node.text.decode('utf-8', errors='ignore') + if variable_name in declaration_text: + # Check if it's a public_metric_groups declaration + if 'public_metric_groups' in declaration_text: + return "external" + elif 'metric_groups' in declaration_text and 'public' not in declaration_text: + return "internal" + + # Search children recursively + for child in node.children: + result = search_for_variable_declaration(child) + if result: + return result + + return None + + # First search the current file + result = search_for_variable_declaration(root_node) + if result: + return result + + # If not found in current file, try to search the corresponding header file + if file_path and str(file_path).endswith('.cc'): + header_path = str(file_path).replace('.cc', '.h') + try: + header_content = get_file_contents(header_path) + if header_content and variable_name.encode() in header_content: + header_text = header_content.decode('utf-8', errors='ignore') + logger.debug(f"Searching header file: {header_path}") + if f'public_metric_groups {variable_name}' in header_text: + logger.debug(f"Found {variable_name} as public_metric_groups in header -> external") + return "external" + elif f'metric_groups {variable_name}' in header_text and 'public' not in header_text: + logger.debug(f"Found {variable_name} as metric_groups in header -> internal") + return "internal" + except Exception as e: + logger.debug(f"Could not read header file {header_path}: {e}") + + # Default fallback based on variable name patterns + if variable_name in ['_public_metrics', '_jobs_metrics', '_service_metrics', '_probe_metrics']: + logger.debug(f"Using name-based fallback: {variable_name} -> external") + return "external" + elif variable_name in ['_internal_metrics', '_metrics']: + logger.debug(f"Using name-based fallback: {variable_name} -> internal") + return "internal" + else: + logger.debug(f"Unknown variable pattern: {variable_name}, defaulting to external") + return "external" + + +def find_group_name_and_type_from_ast(metric_call_expr_node, file_path=None): """ Traverse up the AST from a metric definition to find the enclosing add_group call and extract its name and metric type (internal/external). @@ -142,21 +207,20 @@ def find_group_name_and_type_from_ast(metric_call_expr_node): if function_node and function_node.text.decode('utf-8').endswith('.add_group'): function_text = function_node.text.decode('utf-8') - # Determine metric type based on the object being called - metric_type = "external" # default - if '_metrics.add_group' in function_text and 'public' not in function_text: - # This is likely internal_metric_groups or just _metrics (internal) - metric_type = "internal" - elif '_public_metrics.add_group' in function_text or 'public_metric_groups' in function_text: - # This is public_metric_groups (external) - metric_type = "external" + # Extract the variable name from the add_group call (e.g., "_service_metrics" from "_service_metrics.add_group") + variable_name = function_text.replace('.add_group', '') + logger.debug(f"Found add_group call with variable: {variable_name}") + + # Determine metric type by searching for the variable declaration + metric_type = determine_metric_type_from_variable(current_node, variable_name, file_path) + logger.debug(f"Determined metric_type: {metric_type} for variable: {variable_name}") # This is an add_group call. Now, get its arguments. args_node = current_node.child_by_field_name('arguments') if not args_node or args_node.named_child_count == 0: continue - # The first argument should be prometheus_sanitize::metrics_name(...) + # The first argument should be prometheus_sanitize::metrics_name(...) or a variable first_arg_node = args_node.named_children[0] # Check if this argument is a call to prometheus_sanitize::metrics_name @@ -171,32 +235,539 @@ def find_group_name_and_type_from_ast(metric_call_expr_node): if group_name_node.type == 'string_literal': group_name = unquote_string(group_name_node.text.decode('utf-8')) return group_name, metric_type + elif group_name_node.type == 'identifier': + # The argument to metrics_name is a variable, resolve it + inner_var_name = group_name_node.text.decode('utf-8') + logger.debug(f"Found variable in metrics_name call: {inner_var_name}") + + # Try all our resolution strategies for this variable + resolved_value = resolve_variable_in_local_scope(current_node, inner_var_name) + if not resolved_value: + resolved_value = resolve_variable_declaration(current_node, inner_var_name) + if not resolved_value: + resolved_value = resolve_variable_forward_in_function(current_node, inner_var_name) + if not resolved_value: + resolved_value = find_any_group_name_in_file(current_node) + + if resolved_value: + logger.debug(f"Resolved metrics_name variable {inner_var_name} to: {resolved_value}") + return resolved_value, metric_type + else: + logger.error(f"Could not resolve metrics_name variable: {inner_var_name}") + # EMERGENCY FALLBACK: Try to guess from common patterns + if inner_var_name == "cluster_metric_prefix": + logger.warning("Using emergency fallback for cluster_metric_prefix -> 'cluster'") + return "cluster", metric_type # Handle simple string literal as group name elif first_arg_node.type == 'string_literal': group_name = unquote_string(first_arg_node.text.decode('utf-8')) return group_name, metric_type + # Handle variable reference (like group_name) + elif first_arg_node.type == 'identifier': + variable_name = first_arg_node.text.decode('utf-8') + logger.debug(f"Found variable reference: {variable_name} at line {first_arg_node.start_point[0] + 1}") + + # Try multiple strategies to resolve the variable + group_name = None + + # Strategy 1: Search in the immediate local scope first + group_name = resolve_variable_in_local_scope(current_node, variable_name) + if group_name: + logger.debug(f"Resolved variable {variable_name} locally to: {group_name}") + return group_name, metric_type + + # Strategy 2: Search in broader scopes + group_name = resolve_variable_declaration(current_node, variable_name) + if group_name: + logger.debug(f"Resolved variable {variable_name} in broader scope to: {group_name}") + return group_name, metric_type + + # Strategy 3: Search the entire function/method + group_name = resolve_variable_in_function_scope(current_node, variable_name) + if group_name: + logger.debug(f"Resolved variable {variable_name} in function scope to: {group_name}") + return group_name, metric_type + + # Strategy 4: Search forward in the function for variable declarations + group_name = resolve_variable_forward_in_function(current_node, variable_name) + if group_name: + logger.debug(f"Found variable {variable_name} declared later in function: {group_name}") + return group_name, metric_type + + # Strategy 5: Last resort - search entire file for any group_name variable + if variable_name == "group_name": + group_name = find_any_group_name_in_file(current_node) + if group_name: + logger.debug(f"Found fallback group_name in file: {group_name}") + return group_name, metric_type + + logger.error(f"CRITICAL: Could not resolve variable '{variable_name}' - this should not happen!") + + # EMERGENCY FALLBACK: Hard-coded common patterns + if variable_name == "group_name": + # Return a placeholder that can be manually reviewed + logger.warning(f"Using emergency fallback for group_name - returning 'unknown'") + return "unknown", metric_type + + # Add debugging to see what scopes we searched + logger.debug(f"Current node type: {current_node.type}, parent: {current_node.parent.type if current_node.parent else 'None'}") current_node = current_node.parent return None, "external" # Default to external if not found +def resolve_variable_declaration(start_node, variable_name): + """ + Search for a variable declaration within the current scope and enclosing scopes. + Looks for patterns like: const auto group_name = prometheus_sanitize::metrics_name("..."); + """ + logger.debug(f"Searching for variable '{variable_name}' starting from node type: {start_node.type}") + + # Search from current scope up to the translation unit + scope_node = start_node + + # Keep searching in broader scopes until we find the variable or reach the top + while scope_node: + logger.debug(f"Searching in scope: {scope_node.type}") + + # Search for variable declarations in the current scope + def search_declarations(node, depth=0): + indent = " " * depth + logger.debug(f"{indent}Checking node type: {node.type}") + + if node.type == 'declaration': + logger.debug(f"{indent}Found declaration: {node.text.decode('utf-8')[:100]}...") + # Look for variable declarators + for child in node.children: + if child.type == 'init_declarator': + declarator = child.child_by_field_name('declarator') + initializer = child.child_by_field_name('value') + + if declarator and initializer: + # Check if this is our variable + declarator_text = declarator.text.decode('utf-8') + logger.debug(f"{indent} Declarator: {declarator_text}") + if variable_name in declarator_text: + logger.debug(f"{indent} Found matching variable!") + # Check if the initializer is a call to prometheus_sanitize::metrics_name + if initializer.type == 'call_expression': + func_node = initializer.child_by_field_name('function') + if func_node and '::metrics_name' in func_node.text.decode('utf-8'): + args_node = initializer.child_by_field_name('arguments') + if args_node and args_node.named_child_count > 0: + first_arg = args_node.named_children[0] + if first_arg.type == 'string_literal': + result = unquote_string(first_arg.text.decode('utf-8')) + logger.debug(f"{indent} Resolved to: {result}") + return result + + # Recursively search all child nodes + for child in node.children: + result = search_declarations(child, depth + 1) + if result: + return result + + return None + + # Search in the current scope + result = search_declarations(scope_node) + if result: + return result + + # Move to parent scope + if scope_node.type == 'translation_unit': + # We've reached the top level, stop here + logger.debug("Reached translation unit, stopping search") + break + scope_node = scope_node.parent + if scope_node: + logger.debug(f"Moving to parent scope: {scope_node.type}") + + logger.debug(f"Variable '{variable_name}' not found in any scope") + return None + + +def resolve_variable_in_local_scope(start_node, variable_name): + """ + Search for a variable declaration in the immediate local scope around the add_group call. + This handles cases where the variable is declared just before the add_group call. + """ + logger.debug(f"Searching for variable '{variable_name}' in local scope") + + # First, try to find the enclosing function/method + func_node = start_node + while func_node and func_node.type not in ['function_definition', 'method_definition']: + func_node = func_node.parent + + if not func_node: + logger.debug("No function/method definition found") + return None + + # Get the function body (compound_statement) + body_node = None + for child in func_node.children: + if child.type == 'compound_statement': + body_node = child + break + + if not body_node: + logger.debug("No function body found") + return None + + # Search all declarations in the function body + def search_in_node(node): + if node.type == 'declaration': + # Check for auto group_name = prometheus_sanitize::metrics_name(...); + for child in node.children: + if child.type == 'init_declarator': + declarator = child.child_by_field_name('declarator') + initializer = child.child_by_field_name('value') + + if declarator and initializer: + # Handle 'auto' type declarations + if declarator.type == 'identifier' and declarator.text.decode('utf-8') == variable_name: + # Found our variable! + if initializer.type == 'call_expression': + func_node = initializer.child_by_field_name('function') + if func_node and '::metrics_name' in func_node.text.decode('utf-8'): + args_node = initializer.child_by_field_name('arguments') + if args_node and args_node.named_child_count > 0: + first_arg = args_node.named_children[0] + if first_arg.type == 'string_literal': + return unquote_string(first_arg.text.decode('utf-8')) + + # Recursively search children + for child in node.children: + result = search_in_node(child) + if result: + return result + + return None + + return search_in_node(body_node) + + +def resolve_variable_in_function_scope(start_node, variable_name): + """ + Search for a variable declaration within the entire function scope. + This is the most aggressive search strategy. + """ + logger.debug(f"Searching for variable '{variable_name}' in function scope") + + # Go up to the function definition + func_node = start_node + while func_node and func_node.type not in ['function_definition', 'lambda_expression', 'method_definition']: + func_node = func_node.parent + + if not func_node: + logger.debug("No function definition found for function scope search") + return None + + logger.debug(f"Found function node: {func_node.type}") + + # Search the entire function body recursively + def search_in_function(node, depth=0): + indent = " " * depth + logger.debug(f"{indent}Searching node type: {node.type}") + + # Print the text for declarations to help debug + if node.type in ['declaration', 'expression_statement']: + text = node.text.decode('utf-8')[:100] + logger.debug(f"{indent}Found {node.type}: {text}...") + + if node.type == 'declaration': + result = extract_variable_from_declaration(node, variable_name) + if result: + logger.debug(f"{indent}Found variable in declaration: {result}") + return result + elif node.type == 'expression_statement': + # Some variable declarations might be parsed as expression statements + for child in node.children: + if child.type == 'assignment_expression': + left = child.child_by_field_name('left') + right = child.child_by_field_name('right') + if left and right and left.text.decode('utf-8') == variable_name: + if right.type == 'call_expression': + func_call = right.child_by_field_name('function') + if func_call and '::metrics_name' in func_call.text.decode('utf-8'): + args_node = right.child_by_field_name('arguments') + if args_node and args_node.named_child_count > 0: + first_arg = args_node.named_children[0] + if first_arg.type == 'string_literal': + result = unquote_string(first_arg.text.decode('utf-8')) + logger.debug(f"{indent}Found variable in assignment: {result}") + return result + + # Search all children + for child in node.children: + result = search_in_function(child, depth + 1) + if result: + return result + + return None + + return search_in_function(func_node) + + +def resolve_variable_forward_in_function(start_node, variable_name): + """ + Search forward from the current position for variable declarations. + This handles cases where variables are declared after they're referenced in metric definitions + but before the add_group call. + """ + logger.debug(f"Searching forward for variable '{variable_name}'") + + # Go up to the function definition + func_node = start_node + while func_node and func_node.type not in ['function_definition', 'lambda_expression', 'method_definition']: + func_node = func_node.parent + + if not func_node: + logger.debug("No function definition found for forward search") + return None + + # Find the function body + body_node = None + for child in func_node.children: + if child.type == 'compound_statement': + body_node = child + break + + if not body_node: + logger.debug("No function body found for forward search") + return None + + # Search through all statements in the function body + for statement in body_node.children: + if statement.type == 'declaration': + result = extract_variable_from_declaration(statement, variable_name) + if result: + logger.debug(f"Found forward declaration: {result}") + return result + elif statement.type == 'expression_statement': + # Check for assignment expressions + for child in statement.children: + if child.type == 'assignment_expression': + left = child.child_by_field_name('left') + right = child.child_by_field_name('right') + if left and right and left.text.decode('utf-8') == variable_name: + if right.type == 'call_expression': + func_call = right.child_by_field_name('function') + if func_call and '::metrics_name' in func_call.text.decode('utf-8'): + args_node = right.child_by_field_name('arguments') + if args_node and args_node.named_child_count > 0: + first_arg = args_node.named_children[0] + if first_arg.type == 'string_literal': + result = unquote_string(first_arg.text.decode('utf-8')) + logger.debug(f"Found forward assignment: {result}") + return result + + return None + + +def extract_variable_from_declaration(declaration_node, variable_name): + """ + Extract the value of a variable from a declaration node if it matches the variable name. + Handles patterns like: + - const auto group_name = prometheus_sanitize::metrics_name("..."); + - constexpr static auto cluster_metric_prefix = "cluster"; + """ + for child in declaration_node.children: + if child.type == 'init_declarator': + declarator = child.child_by_field_name('declarator') + initializer = child.child_by_field_name('value') + + if declarator and initializer: + declarator_text = declarator.text.decode('utf-8') + if variable_name in declarator_text: + # Check if the initializer is a call to prometheus_sanitize::metrics_name + if initializer.type == 'call_expression': + func_node = initializer.child_by_field_name('function') + if func_node and '::metrics_name' in func_node.text.decode('utf-8'): + args_node = initializer.child_by_field_name('arguments') + if args_node and args_node.named_child_count > 0: + first_arg = args_node.named_children[0] + if first_arg.type == 'string_literal': + return unquote_string(first_arg.text.decode('utf-8')) + # Also check for simple string literal assignment (like constexpr static auto cluster_metric_prefix = "cluster") + elif initializer.type == 'string_literal': + return unquote_string(initializer.text.decode('utf-8')) + return None + + +def find_any_group_name_in_file(start_node): + """ + Last resort: search the entire file for any variable that's assigned + a prometheus_sanitize::metrics_name value, regardless of variable name. + """ + logger.debug("Searching entire file for any metrics_name assignment") + + # Go to the root of the file + root_node = start_node + while root_node.parent: + root_node = root_node.parent + + # Search the entire file for any prometheus_sanitize::metrics_name call + def search_entire_file(node): + if node.type == 'declaration': + # Look for any variable declared with prometheus_sanitize::metrics_name + for child in node.children: + if child.type == 'init_declarator': + declarator = child.child_by_field_name('declarator') + initializer = child.child_by_field_name('value') + + if declarator and initializer: + # Check if the initializer is a call to prometheus_sanitize::metrics_name + if initializer.type == 'call_expression': + func_node = initializer.child_by_field_name('function') + if func_node and '::metrics_name' in func_node.text.decode('utf-8'): + args_node = initializer.child_by_field_name('arguments') + if args_node and args_node.named_child_count > 0: + first_arg = args_node.named_children[0] + if first_arg.type == 'string_literal': + result = unquote_string(first_arg.text.decode('utf-8')) + declarator_text = declarator.text.decode('utf-8') + logger.debug(f"Found metrics_name assignment in file: {declarator_text} = {result}") + return result + + # Also check for assignment expressions + if node.type == 'assignment_expression': + left = node.child_by_field_name('left') + right = node.child_by_field_name('right') + if left and right: + if right.type == 'call_expression': + func_node = right.child_by_field_name('function') + if func_node and '::metrics_name' in func_node.text.decode('utf-8'): + args_node = right.child_by_field_name('arguments') + if args_node and args_node.named_child_count > 0: + first_arg = args_node.named_children[0] + if first_arg.type == 'string_literal': + result = unquote_string(first_arg.text.decode('utf-8')) + left_text = left.text.decode('utf-8') + logger.debug(f"Found metrics_name assignment in file: {left_text} = {result}") + return result + + # Search all children recursively + for child in node.children: + result = search_entire_file(child) + if result: + return result + + return None + + return search_entire_file(root_node) + + +def find_any_metrics_name_in_file(start_node, file_path): + """ + Enhanced search: find ANY variable in the file that's assigned a prometheus_sanitize::metrics_name value. + This handles cases where the variable name is not 'group_name' (e.g., 'cluster_metrics_name'). + """ + logger.debug(f"Enhanced file-wide search for metrics_name declarations in {file_path}") + + # Go to the root of the file + root_node = start_node + while root_node.parent: + root_node = root_node.parent + + def search_any_metrics_name(node): + if node.type == 'declaration': + # Look for any variable declared with prometheus_sanitize::metrics_name + for child in node.children: + if child.type == 'init_declarator': + declarator = child.child_by_field_name('declarator') + initializer = child.child_by_field_name('value') + + if declarator and initializer: + if initializer.type == 'call_expression': + func_node = initializer.child_by_field_name('function') + if func_node and '::metrics_name' in func_node.text.decode('utf-8'): + args_node = initializer.child_by_field_name('arguments') + if args_node and args_node.named_child_count > 0: + first_arg = args_node.named_children[0] + if first_arg.type == 'string_literal': + result = unquote_string(first_arg.text.decode('utf-8')) + var_name = declarator.text.decode('utf-8') + logger.debug(f"Found metrics_name declaration: {var_name} = '{result}'") + return result + + # Also check for assignment expressions (not just declarations) + if node.type == 'assignment_expression': + left = node.child_by_field_name('left') + right = node.child_by_field_name('right') + if left and right: + if right.type == 'call_expression': + func_node = right.child_by_field_name('function') + if func_node and '::metrics_name' in func_node.text.decode('utf-8'): + args_node = right.child_by_field_name('arguments') + if args_node and args_node.named_child_count > 0: + first_arg = args_node.named_children[0] + if first_arg.type == 'string_literal': + result = unquote_string(first_arg.text.decode('utf-8')) + var_name = left.text.decode('utf-8') + logger.debug(f"Found metrics_name assignment: {var_name} = '{result}'") + return result + + # Search all children recursively + for child in node.children: + result = search_any_metrics_name(child) + if result: + return result + + return None + + return search_any_metrics_name(root_node) + + +def infer_group_name_from_path(file_path): + """ + Programmatic inference of group names from file paths with common patterns. + """ + path_str = str(file_path).lower() + file_parts = path_str.split('/') + + # Define path-based inference rules + inference_rules = [ + # Pattern: (path_contains, additional_condition, group_name) + (['kafka', 'quota'], lambda p: 'quota' in p, "kafka:quotas"), + (['datalake', 'translation'], lambda p: 'translation' in p, "iceberg:translation"), + (['iceberg', 'rest_client'], lambda p: 'rest_client' in p, "iceberg:rest_client"), + (['cluster', 'partition'], lambda p: 'partition' in p, "cluster:partition"), + (['debug_bundle'], lambda p: True, "debug_bundle"), + (['kafka'], lambda p: True, "kafka"), + (['cluster'], lambda p: True, "cluster"), + (['iceberg'], lambda p: True, "iceberg"), + (['storage'], lambda p: 'cloud' in p, "cloud_storage"), + ] + + # Apply inference rules + for path_keywords, condition, group_name in inference_rules: + if all(keyword in file_parts for keyword in path_keywords) and condition(path_str): + return group_name + + # Default fallback + return "unknown" + + def find_group_name_from_ast(metric_call_expr_node): """ Traverse up the AST from a metric definition to find the enclosing add_group call and extract its name. This is more reliable than regex. """ - group_name, _ = find_group_name_and_type_from_ast(metric_call_expr_node) + group_name, _ = find_group_name_and_type_from_ast(metric_call_expr_node, None) return group_name def construct_full_metric_name(group_name, metric_name, metric_type="external"): """Construct the full Prometheus metric name from group and metric name""" + # Add debug logging if not group_name or group_name == "unknown": # Fallback based on metric type if metric_type == "internal": - return f"vectorized_{metric_name}" + result = f"vectorized_{metric_name}" else: - return f"redpanda_{metric_name}" + result = f"redpanda_{metric_name}" + return result # Sanitize the group name: replace special characters with underscores. sanitized_group = group_name.replace(':', '_').replace('-', '_') @@ -216,26 +787,147 @@ def construct_full_metric_name(group_name, metric_name, metric_type="external"): full_group_name = sanitized_group # The full metric name is: _ - return f"{full_group_name}_{metric_name}" + result = f"{full_group_name}_{metric_name}" + return result + + +def parse_seastar_replicated_metrics(tree_root, source_code, file_path): + """Parse seastar replicated metrics from seastar::metrics::replicate_metric_families calls""" + metrics_bag = MetricsBag() + + # Look for seastar::metrics::replicate_metric_families calls + def find_replicate_calls(node): + if node.type == 'call_expression': + function_node = node.child_by_field_name('function') + if function_node and 'replicate_metric_families' in function_node.text.decode('utf-8'): + logger.debug(f"Found replicate_metric_families call in {file_path}") + args_node = node.child_by_field_name('arguments') + if args_node: + # Look for the array of metric names + for child in args_node.children: + if child.type == 'initializer_list': + # This is the array of {"metric_name", handle} pairs + for item in child.children: + if item.type == 'initializer_list': + # Each item is {"metric_name", handle} + metric_items = [c for c in item.children if c.type == 'string_literal'] + if metric_items: + metric_name = unquote_string(metric_items[0].text.decode('utf-8')) + if metric_name: + logger.debug(f"Found replicated seastar metric: {metric_name}") + # Seastar metrics are typically in the "application" group + full_metric_name = f"redpanda_{metric_name}" + + metrics_bag.add_metric( + name=metric_name, + metric_type="gauge", # Most seastar metrics are gauges + description=f"Seastar metric: {metric_name}", + labels=[], + file=str(file_path), + constructor="seastar_replicated", + group_name="application", + full_name=full_metric_name, + internal_external_type="public" + ) + + # Search children recursively + for child in node.children: + find_replicate_calls(child) + + find_replicate_calls(tree_root) + return metrics_bag + + +def parse_direct_seastar_metrics(tree_root, source_code, file_path): + """Parse direct ss::metrics calls like sm::make_gauge""" + metrics_bag = MetricsBag() + + # Look for ss::metrics or sm:: calls + def find_direct_seastar_calls(node): + if node.type == 'call_expression': + function_node = node.child_by_field_name('function') + if function_node: + function_text = function_node.text.decode('utf-8') + + # Check for sm::make_* or ss::metrics::make_* patterns + seastar_type = None + if 'sm::make_gauge' in function_text or 'ss::metrics::make_gauge' in function_text: + seastar_type = 'gauge' + elif 'sm::make_counter' in function_text or 'ss::metrics::make_counter' in function_text: + seastar_type = 'counter' + elif 'sm::make_histogram' in function_text or 'ss::metrics::make_histogram' in function_text: + seastar_type = 'histogram' + + if seastar_type: + args_node = node.child_by_field_name('arguments') + if args_node and args_node.named_child_count > 0: + # First argument is typically the metric name + first_arg = args_node.named_children[0] + if first_arg.type == 'string_literal': + metric_name = unquote_string(first_arg.text.decode('utf-8')) + + # Try to find description from subsequent arguments + description = f"Seastar metric: {metric_name}" + for i in range(1, args_node.named_child_count): + arg = args_node.named_children[i] + if arg.type == 'call_expression': + # Look for sm::description() calls + desc_func = arg.child_by_field_name('function') + if desc_func and 'description' in desc_func.text.decode('utf-8'): + desc_args = arg.child_by_field_name('arguments') + if desc_args and desc_args.named_child_count > 0: + desc_arg = desc_args.named_children[0] + if desc_arg.type == 'string_literal': + description = unquote_string(desc_arg.text.decode('utf-8')) + break + + logger.debug(f"Found direct seastar metric: {metric_name}") + full_metric_name = f"redpanda_{metric_name}" + + metrics_bag.add_metric( + name=metric_name, + metric_type=seastar_type, + description=description, + labels=[], + file=str(file_path), + constructor=f"seastar_{seastar_type}", + group_name="application", + full_name=full_metric_name, + internal_external_type="public" + ) + + # Search children recursively + for child in node.children: + find_direct_seastar_calls(child) + + find_direct_seastar_calls(tree_root) + return metrics_bag def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace=None): """Parse a single C++ file for metrics definitions""" # Only show debug info in verbose mode - # logger.debug(f"Parsing file: {file_path}") source_code = get_file_contents(file_path) if not source_code: return MetricsBag() - + try: tree = treesitter_parser.parse(source_code) except Exception as e: logger.warning(f"Failed to parse {file_path}: {e}") return MetricsBag() - + metrics_bag = MetricsBag() + # First, parse seastar metrics + seastar_replicated = parse_seastar_replicated_metrics(tree.root_node, source_code, file_path) + metrics_bag.merge(seastar_replicated) + + seastar_direct = parse_direct_seastar_metrics(tree.root_node, source_code, file_path) + metrics_bag.merge(seastar_direct) + + # Then parse regular prometheus metrics # A general query to find all function calls simple_query = cpp_language.query("(call_expression) @call") @@ -272,19 +964,10 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= continue # Use robust AST traversal to find the group name and metric type - group_name, internal_external_type = find_group_name_and_type_from_ast(call_expr) - - # Only show warnings for missing groups in verbose mode - # if group_name: - # logger.debug(f"Found group_name: {group_name} for metric: {metric_name}") - # else: - # logger.warning(f"Could not find group_name for metric '{metric_name}' at line {call_expr.start_point[0] + 1}") + group_name, internal_external_type = find_group_name_and_type_from_ast(call_expr, file_path) full_metric_name = construct_full_metric_name(group_name, metric_name, internal_external_type) - # Commented out to reduce debug noise - # logger.debug(f"Processing metric '{metric_name}': group_name='{group_name}', metric_type='{internal_external_type}', full_name='{full_metric_name}'") - # Get code context for labels start_byte = call_expr.start_byte end_byte = call_expr.end_byte @@ -294,6 +977,25 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= labels = extract_labels_from_code(code_context) + # CRITICAL SAFEGUARD: Never allow null group names + if group_name is None: + logger.error(f"CRITICAL: group_name is None for metric '{metric_name}' in {file_path}") + logger.error(f"File context: {metric_name} at line {call_expr.start_point[0] + 1}") + + # Enhanced emergency fallback: try to find any metrics_name declaration in the file + group_name = find_any_metrics_name_in_file(call_expr, file_path) + + if not group_name: + # Last resort: programmatic file path inference + group_name = infer_group_name_from_path(file_path) + logger.warning(f"Emergency fallback: inferred group_name='{group_name}' from file path") + else: + logger.warning(f"Emergency fallback: found group_name='{group_name}' via file-wide search") + + # CRITICAL: Recalculate full_metric_name with the corrected group_name + full_metric_name = construct_full_metric_name(group_name, metric_name, internal_external_type) + logger.debug(f"Recalculated full_metric_name after emergency fallback: {full_metric_name}") + metrics_bag.add_metric( name=metric_name, metric_type=metric_type, @@ -307,9 +1009,6 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= internal_external_type=internal_external_type # Add the new field ) - # Commented out to reduce noise - # logger.debug(f"Found metric: {metric_name} ({metric_type}) -> {full_metric_name}") - except Exception as e: logger.warning(f"Query failed on {file_path}: {e}") @@ -408,7 +1107,6 @@ def collect_string_info(node): # Filter out descriptions with unresolved format placeholders if description and '{}' in description: - logger.debug(f"Filtering out description with unresolved placeholders: '{description}'") description = "" return metric_name, description From 090176a6aabc0197cd62a1e46e96f8f4c0e00557 Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 23 Jul 2025 17:48:52 -0300 Subject: [PATCH 20/21] fix seastar generation --- tools/metrics-extractor/metrics_parser.py | 124 ++++++++++++++-------- 1 file changed, 81 insertions(+), 43 deletions(-) diff --git a/tools/metrics-extractor/metrics_parser.py b/tools/metrics-extractor/metrics_parser.py index d5f49ca..a9d4c81 100644 --- a/tools/metrics-extractor/metrics_parser.py +++ b/tools/metrics-extractor/metrics_parser.py @@ -795,40 +795,44 @@ def parse_seastar_replicated_metrics(tree_root, source_code, file_path): """Parse seastar replicated metrics from seastar::metrics::replicate_metric_families calls""" metrics_bag = MetricsBag() - # Look for seastar::metrics::replicate_metric_families calls + # Look ONLY for seastar::metrics::replicate_metric_families calls def find_replicate_calls(node): if node.type == 'call_expression': function_node = node.child_by_field_name('function') - if function_node and 'replicate_metric_families' in function_node.text.decode('utf-8'): - logger.debug(f"Found replicate_metric_families call in {file_path}") - args_node = node.child_by_field_name('arguments') - if args_node: - # Look for the array of metric names - for child in args_node.children: - if child.type == 'initializer_list': - # This is the array of {"metric_name", handle} pairs - for item in child.children: - if item.type == 'initializer_list': - # Each item is {"metric_name", handle} - metric_items = [c for c in item.children if c.type == 'string_literal'] - if metric_items: - metric_name = unquote_string(metric_items[0].text.decode('utf-8')) - if metric_name: - logger.debug(f"Found replicated seastar metric: {metric_name}") - # Seastar metrics are typically in the "application" group - full_metric_name = f"redpanda_{metric_name}" - - metrics_bag.add_metric( - name=metric_name, - metric_type="gauge", # Most seastar metrics are gauges - description=f"Seastar metric: {metric_name}", - labels=[], - file=str(file_path), - constructor="seastar_replicated", - group_name="application", - full_name=full_metric_name, - internal_external_type="public" - ) + if function_node: + function_text = function_node.text.decode('utf-8') + # Be very specific - must be exactly replicate_metric_families + if 'replicate_metric_families' in function_text and 'seastar::metrics::' in function_text: + logger.debug(f"Found seastar replicate_metric_families call in {file_path}") + args_node = node.child_by_field_name('arguments') + if args_node: + # Look for the array of metric names + for child in args_node.children: + if child.type == 'initializer_list': + # This is the array of {"metric_name", handle} pairs + for item in child.children: + if item.type == 'initializer_list': + # Each item is {"metric_name", handle} + metric_items = [c for c in item.children if c.type == 'string_literal'] + if metric_items: + metric_name = unquote_string(metric_items[0].text.decode('utf-8')) + if metric_name: + logger.debug(f"Found replicated seastar metric: {metric_name}") + # Seastar metrics are typically in the "application" group + full_metric_name = f"redpanda_{metric_name}" + + metrics_bag.add_metric( + name=metric_name, + metric_type="gauge", # Most seastar metrics are gauges + description=f"Seastar replicated metric: {metric_name}", + labels=[], + file=str(file_path), + constructor="seastar_replicated", + group_name="application", + full_name=full_metric_name, + internal_external_type="public", + line_number=node.start_point[0] + 1 + ) # Search children recursively for child in node.children: @@ -839,26 +843,58 @@ def find_replicate_calls(node): def parse_direct_seastar_metrics(tree_root, source_code, file_path): - """Parse direct ss::metrics calls like sm::make_gauge""" + """Parse direct ss::metrics calls like sm::make_gauge ONLY in specific contexts""" metrics_bag = MetricsBag() - # Look for ss::metrics or sm:: calls + # Look for ss::metrics or sm:: calls but ONLY in the application.cc context + # This is a very specific pattern that should not interfere with regular metrics + if 'application.cc' not in str(file_path): + return metrics_bag # Only process application.cc for direct seastar metrics + def find_direct_seastar_calls(node): if node.type == 'call_expression': function_node = node.child_by_field_name('function') if function_node: function_text = function_node.text.decode('utf-8') - # Check for sm::make_* or ss::metrics::make_* patterns + # Be very specific - must be sm:: prefix AND in the right context seastar_type = None - if 'sm::make_gauge' in function_text or 'ss::metrics::make_gauge' in function_text: + if function_text == 'sm::make_gauge': seastar_type = 'gauge' - elif 'sm::make_counter' in function_text or 'ss::metrics::make_counter' in function_text: + elif function_text == 'sm::make_counter': seastar_type = 'counter' - elif 'sm::make_histogram' in function_text or 'ss::metrics::make_histogram' in function_text: + elif function_text == 'sm::make_histogram': seastar_type = 'histogram' + # Also check for direct ss::metrics calls + if not seastar_type: + if function_text == 'ss::metrics::make_gauge': + seastar_type = 'gauge' + elif function_text == 'ss::metrics::make_counter': + seastar_type = 'counter' + elif function_text == 'ss::metrics::make_histogram': + seastar_type = 'histogram' + if seastar_type: + # Additional check: must be in a specific function context + # Look for setup_public_metrics or similar function + current = node.parent + in_correct_function = False + while current: + if current.type == 'function_definition': + # Check if this is the setup_public_metrics function + for child in current.children: + if child.type == 'function_declarator': + func_name = child.text.decode('utf-8') + if 'setup_public_metrics' in func_name: + in_correct_function = True + break + break + current = current.parent + + if not in_correct_function: + return # Skip if not in the right function + args_node = node.child_by_field_name('arguments') if args_node and args_node.named_child_count > 0: # First argument is typically the metric name @@ -867,7 +903,7 @@ def find_direct_seastar_calls(node): metric_name = unquote_string(first_arg.text.decode('utf-8')) # Try to find description from subsequent arguments - description = f"Seastar metric: {metric_name}" + description = f"Seastar direct metric: {metric_name}" for i in range(1, args_node.named_child_count): arg = args_node.named_children[i] if arg.type == 'call_expression': @@ -893,7 +929,8 @@ def find_direct_seastar_calls(node): constructor=f"seastar_{seastar_type}", group_name="application", full_name=full_metric_name, - internal_external_type="public" + internal_external_type="public", + line_number=node.start_point[0] + 1 ) # Search children recursively @@ -920,12 +957,13 @@ def parse_cpp_file(file_path, treesitter_parser, cpp_language, filter_namespace= metrics_bag = MetricsBag() + # TODO: Add seastar metrics parsing later - currently disabled to avoid contamination # First, parse seastar metrics - seastar_replicated = parse_seastar_replicated_metrics(tree.root_node, source_code, file_path) - metrics_bag.merge(seastar_replicated) + # seastar_replicated = parse_seastar_replicated_metrics(tree.root_node, source_code, file_path) + # metrics_bag.merge(seastar_replicated) - seastar_direct = parse_direct_seastar_metrics(tree.root_node, source_code, file_path) - metrics_bag.merge(seastar_direct) + # seastar_direct = parse_direct_seastar_metrics(tree.root_node, source_code, file_path) + # metrics_bag.merge(seastar_direct) # Then parse regular prometheus metrics # A general query to find all function calls From 9c382b1315cce88e24ae08754e5a8260496134da Mon Sep 17 00:00:00 2001 From: Paulo Borges Date: Wed, 23 Jul 2025 17:49:07 -0300 Subject: [PATCH 21/21] add a compare with original script --- tools/metrics-extractor/compare_original.py | 818 ++++++++++++++++++++ 1 file changed, 818 insertions(+) create mode 100644 tools/metrics-extractor/compare_original.py diff --git a/tools/metrics-extractor/compare_original.py b/tools/metrics-extractor/compare_original.py new file mode 100644 index 0000000..b3ad4db --- /dev/null +++ b/tools/metrics-extractor/compare_original.py @@ -0,0 +1,818 @@ +#!/usr/bin/env python3 +""" +Dual Metrics Documentation Diff Tool + +This tool compares Prometheus metrics documentation files in AsciiDoc format +for both public and internal metrics, identifying differences in metrics, +descriptions, types, and labels. + +Usage: + python metrics_diff.py --original-public orig_pub.adoc --generated-public gen_pub.adoc + python metrics_diff.py --original-internal orig_int.adoc --generated-internal gen_int.adoc + python metrics_diff.py --original-public orig_pub.adoc --generated-public gen_pub.adoc --original-internal orig_int.adoc --generated-internal gen_int.adoc +""" + +import re +import json +import argparse +import sys +from typing import Dict, List, Set, Tuple, Optional +from dataclasses import dataclass, field +from collections import defaultdict + + +@dataclass +class MetricInfo: + """Structure to hold metric information""" + name: str + description: str = "" + metric_type: str = "" + labels: List[str] = field(default_factory=list) + usage: str = "" + section: str = "" + raw_content: str = "" + + +class MetricsParser: + """Parser for AsciiDoc metrics documentation""" + + def __init__(self, metric_header_level: str = "==="): + """ + Initialize parser with specific header level + metric_header_level: "===" for public metrics, "==" for internal metrics + """ + self.metrics = {} + self.current_section = "" + self.metric_header_level = metric_header_level + + def parse_file(self, content: str) -> Dict[str, MetricInfo]: + """Parse the AsciiDoc content and extract metrics information""" + lines = content.split('\n') + current_metric = None + in_metric_block = False + collecting_description = False + collecting_labels = False + collecting_usage = False + raw_start = 0 + + i = 0 + while i < len(lines): + line = lines[i].strip() + + # Skip empty lines + if not line: + i += 1 + continue + + # Handle different header levels based on metric type + if self.metric_header_level == "===": + # PUBLIC METRICS: == is section, === is metric + if line.startswith('== ') and not line.startswith('=== '): + self.current_section = line[3:].strip() + i += 1 + continue + elif line.startswith('=== '): + # Save previous metric if exists + if current_metric and current_metric.name: + raw_end = i + current_metric.raw_content = '\n'.join(lines[raw_start:raw_end]) + self.metrics[current_metric.name] = current_metric + + metric_name = line[4:].strip() + current_metric = MetricInfo( + name=metric_name, + section=self.current_section + ) + in_metric_block = True + collecting_description = True + collecting_labels = False + collecting_usage = False + raw_start = i + i += 1 + continue + + elif self.metric_header_level == "==": + # INTERNAL METRICS: == is metric (no section headers typically) + if line.startswith('== ') and not line.startswith('=== '): + # Check if this looks like a metric name (starts with vectorized_) + potential_metric = line[3:].strip() + if potential_metric.startswith('vectorized_') or len(potential_metric.split()) == 1: + # This is a metric header + # Save previous metric if exists + if current_metric and current_metric.name: + raw_end = i + current_metric.raw_content = '\n'.join(lines[raw_start:raw_end]) + self.metrics[current_metric.name] = current_metric + + metric_name = potential_metric + current_metric = MetricInfo( + name=metric_name, + section=self.current_section or "Internal Metrics" + ) + in_metric_block = True + collecting_description = True + collecting_labels = False + collecting_usage = False + raw_start = i + i += 1 + continue + else: + # This might be a section header (rare in internal metrics) + self.current_section = potential_metric + i += 1 + continue + + if not in_metric_block or not current_metric: + i += 1 + continue + + # Check for Type specification + if line.startswith('*Type*:'): + current_metric.metric_type = line.split(':', 1)[1].strip() + collecting_description = False + i += 1 + continue + + # Check for Labels section + if line.startswith('*Labels*:'): + collecting_labels = True + collecting_description = False + collecting_usage = False + i += 1 + continue + + # Check for Usage section + if line.startswith('*Usage*:'): + collecting_usage = True + collecting_description = False + collecting_labels = False + i += 1 + continue + + # Check for end of metric (horizontal rule) + if line.startswith('---'): + collecting_description = False + collecting_labels = False + collecting_usage = False + i += 1 + continue + + # Collect content based on current state + if collecting_description and not line.startswith('*') and not line.startswith('- ') and not line.startswith('* '): + if current_metric.description: + current_metric.description += " " + line + else: + current_metric.description = line + + elif collecting_labels: + # Extract label information + if line.startswith('- ') or line.startswith('* '): + label_text = line[2:].strip() + # Clean up label text by removing backticks and extra formatting + label_text = re.sub(r'`([^`]+)`', r'\1', label_text) + current_metric.labels.append(label_text) + elif line and not line.startswith('*'): + # Continue collecting labels if not a new section + if current_metric.labels: + current_metric.labels[-1] += " " + line + + elif collecting_usage: + if not line.startswith('*'): + if current_metric.usage: + current_metric.usage += " " + line + else: + current_metric.usage = line + + i += 1 + + # Don't forget the last metric + if current_metric and current_metric.name: + current_metric.raw_content = '\n'.join(lines[raw_start:]) + self.metrics[current_metric.name] = current_metric + + return self.metrics + + +class MetricsDiff: + """Class to compare two sets of metrics and generate diff report""" + + def __init__(self, original_metrics: Dict[str, MetricInfo], generated_metrics: Dict[str, MetricInfo], metrics_type: str = ""): + self.original = original_metrics + self.generated = generated_metrics + self.metrics_type = metrics_type + + def get_metric_sets(self) -> Tuple[Set[str], Set[str], Set[str], Set[str]]: + """Get sets of metric names for comparison""" + original_names = set(self.original.keys()) + generated_names = set(self.generated.keys()) + + removed = original_names - generated_names + added = generated_names - original_names + common = original_names & generated_names + + return removed, added, common, original_names | generated_names + + def compare_metrics(self) -> Dict: + """Compare metrics and return comprehensive diff report""" + removed, added, common, all_metrics = self.get_metric_sets() + + report = { + 'metrics_type': self.metrics_type, + 'summary': { + 'total_original': len(self.original), + 'total_generated': len(self.generated), + 'removed_count': len(removed), + 'added_count': len(added), + 'common_count': len(common), + 'modified_count': 0 + }, + 'removed_metrics': sorted(list(removed)), + 'added_metrics': sorted(list(added)), + 'modified_metrics': {}, + 'section_changes': self._analyze_section_changes(), + 'type_changes': self._analyze_type_changes(), + 'label_changes': self._analyze_label_changes() + } + + # Analyze modifications in common metrics + modified_count = 0 + for metric_name in common: + original_metric = self.original[metric_name] + generated_metric = self.generated[metric_name] + + changes = self._compare_single_metric(original_metric, generated_metric) + if changes: + report['modified_metrics'][metric_name] = changes + modified_count += 1 + + report['summary']['modified_count'] = modified_count + + return report + + def _compare_single_metric(self, original: MetricInfo, generated: MetricInfo) -> Dict: + """Compare two metrics and return differences""" + changes = {} + + # Compare descriptions + orig_desc = original.description.strip() + gen_desc = generated.description.strip() + if orig_desc != gen_desc: + changes['description'] = { + 'original': orig_desc, + 'generated': gen_desc, + 'length_diff': len(gen_desc) - len(orig_desc) + } + + # Compare types + if original.metric_type != generated.metric_type: + changes['type'] = { + 'original': original.metric_type, + 'generated': generated.metric_type + } + + # Compare labels + original_labels = set(original.labels) + generated_labels = set(generated.labels) + + if original_labels != generated_labels: + changes['labels'] = { + 'removed': sorted(list(original_labels - generated_labels)), + 'added': sorted(list(generated_labels - original_labels)), + 'original_count': len(original_labels), + 'generated_count': len(generated_labels), + 'original_labels': sorted(list(original_labels)), + 'generated_labels': sorted(list(generated_labels)) + } + + # Compare usage + orig_usage = original.usage.strip() + gen_usage = generated.usage.strip() + if orig_usage != gen_usage: + changes['usage'] = { + 'original': orig_usage, + 'generated': gen_usage, + 'original_has_usage': bool(orig_usage), + 'generated_has_usage': bool(gen_usage) + } + + # Compare sections + if original.section != generated.section: + changes['section'] = { + 'original': original.section, + 'generated': generated.section + } + + return changes + + def _analyze_section_changes(self) -> Dict: + """Analyze changes in metric organization by sections""" + original_by_section = defaultdict(list) + generated_by_section = defaultdict(list) + + for name, metric in self.original.items(): + original_by_section[metric.section].append(name) + + for name, metric in self.generated.items(): + generated_by_section[metric.section].append(name) + + section_changes = {} + all_sections = set(original_by_section.keys()) | set(generated_by_section.keys()) + + for section in all_sections: + original_metrics = set(original_by_section.get(section, [])) + generated_metrics = set(generated_by_section.get(section, [])) + + if original_metrics != generated_metrics: + section_changes[section] = { + 'original_count': len(original_metrics), + 'generated_count': len(generated_metrics), + 'removed': sorted(list(original_metrics - generated_metrics)), + 'added': sorted(list(generated_metrics - original_metrics)), + 'moved_in': [], + 'moved_out': [] + } + + # Identify metrics that moved between sections + for metric_name in set(self.original.keys()) & set(self.generated.keys()): + orig_section = self.original[metric_name].section + gen_section = self.generated[metric_name].section + if orig_section != gen_section: + if orig_section in section_changes: + section_changes[orig_section]['moved_out'].append(f"{metric_name} -> {gen_section}") + if gen_section in section_changes: + section_changes[gen_section]['moved_in'].append(f"{metric_name} <- {orig_section}") + + return section_changes + + def _analyze_type_changes(self) -> Dict: + """Analyze changes in metric types""" + type_changes = {} + + for metric_name in set(self.original.keys()) & set(self.generated.keys()): + orig_type = self.original[metric_name].metric_type + gen_type = self.generated[metric_name].metric_type + + if orig_type != gen_type: + type_changes[metric_name] = { + 'original': orig_type, + 'generated': gen_type + } + + return type_changes + + def _analyze_label_changes(self) -> Dict: + """Analyze changes in metric labels across all metrics""" + label_stats = { + 'metrics_with_labels_removed': 0, + 'metrics_with_labels_added': 0, + 'metrics_with_label_changes': 0, + 'total_labels_removed': 0, + 'total_labels_added': 0, + 'common_labels_removed': set(), + 'common_labels_added': set() + } + + for metric_name in set(self.original.keys()) & set(self.generated.keys()): + orig_labels = set(self.original[metric_name].labels) + gen_labels = set(self.generated[metric_name].labels) + + removed_labels = orig_labels - gen_labels + added_labels = gen_labels - orig_labels + + if removed_labels or added_labels: + label_stats['metrics_with_label_changes'] += 1 + + if removed_labels: + label_stats['metrics_with_labels_removed'] += 1 + label_stats['total_labels_removed'] += len(removed_labels) + label_stats['common_labels_removed'].update(removed_labels) + + if added_labels: + label_stats['metrics_with_labels_added'] += 1 + label_stats['total_labels_added'] += len(added_labels) + label_stats['common_labels_added'].update(added_labels) + + # Convert sets to sorted lists for JSON serialization + label_stats['common_labels_removed'] = sorted(list(label_stats['common_labels_removed'])) + label_stats['common_labels_added'] = sorted(list(label_stats['common_labels_added'])) + + return label_stats + + +class DualMetricsReportGenerator: + """Generate combined reports for both public and internal metrics""" + + def __init__(self, public_diff: Optional[MetricsDiff] = None, internal_diff: Optional[MetricsDiff] = None): + self.public_diff = public_diff + self.internal_diff = internal_diff + + def generate_combined_report(self, output_file: str = None) -> str: + """Generate a comprehensive report for both metric types""" + report_lines = [] + report_lines.append("# Metrics Documentation Diff Report") + report_lines.append("=" * 60) + report_lines.append("") + + # Overall summary + total_original = 0 + total_generated = 0 + total_removed = 0 + total_added = 0 + total_modified = 0 + + if self.public_diff: + public_data = self.public_diff.compare_metrics() + total_original += public_data['summary']['total_original'] + total_generated += public_data['summary']['total_generated'] + total_removed += public_data['summary']['removed_count'] + total_added += public_data['summary']['added_count'] + total_modified += public_data['summary']['modified_count'] + + if self.internal_diff: + internal_data = self.internal_diff.compare_metrics() + total_original += internal_data['summary']['total_original'] + total_generated += internal_data['summary']['total_generated'] + total_removed += internal_data['summary']['removed_count'] + total_added += internal_data['summary']['added_count'] + total_modified += internal_data['summary']['modified_count'] + + report_lines.append("## Overall Summary") + report_lines.append(f"- **Total original metrics**: {total_original}") + report_lines.append(f"- **Total generated metrics**: {total_generated}") + report_lines.append(f"- **Net change**: {total_generated - total_original:+d}") + report_lines.append(f"- **Total removed**: {total_removed}") + report_lines.append(f"- **Total added**: {total_added}") + report_lines.append(f"- **Total modified**: {total_modified}") + report_lines.append("") + + # Individual reports + if self.public_diff: + report_lines.append("# PUBLIC METRICS") + report_lines.append("=" * 40) + public_report = self._generate_single_report(self.public_diff, "Public") + report_lines.extend(public_report.split('\n')[3:]) # Skip the title + report_lines.append("") + + if self.internal_diff: + report_lines.append("# INTERNAL METRICS") + report_lines.append("=" * 40) + internal_report = self._generate_single_report(self.internal_diff, "Internal") + report_lines.extend(internal_report.split('\n')[3:]) # Skip the title + report_lines.append("") + + report_text = '\n'.join(report_lines) + + if output_file: + with open(output_file, 'w', encoding='utf-8') as f: + f.write(report_text) + print(f"Combined report saved to {output_file}") + + return report_text + + def _generate_single_report(self, diff_tool: MetricsDiff, metrics_type: str) -> str: + """Generate a report for a single metrics type""" + diff_data = diff_tool.compare_metrics() + + report_lines = [] + report_lines.append(f"# {metrics_type} Metrics Report") + report_lines.append("=" * 40) + report_lines.append("") + + # Summary + summary = diff_data['summary'] + report_lines.append("## Summary") + report_lines.append(f"- **Original metrics count**: {summary['total_original']}") + report_lines.append(f"- **Generated metrics count**: {summary['total_generated']}") + report_lines.append(f"- **Net change**: {summary['total_generated'] - summary['total_original']:+d}") + report_lines.append(f"- **Removed metrics**: {summary['removed_count']}") + report_lines.append(f"- **Added metrics**: {summary['added_count']}") + report_lines.append(f"- **Modified metrics**: {summary['modified_count']}") + report_lines.append(f"- **Unchanged metrics**: {summary['common_count'] - summary['modified_count']}") + report_lines.append("") + + # Label changes summary + label_stats = diff_data['label_changes'] + if label_stats['metrics_with_label_changes'] > 0: + report_lines.append("## Label Changes Summary") + report_lines.append(f"- Metrics with label changes: {label_stats['metrics_with_label_changes']}") + report_lines.append(f"- Total labels removed: {label_stats['total_labels_removed']}") + report_lines.append(f"- Total labels added: {label_stats['total_labels_added']}") + if label_stats['common_labels_removed']: + removed_preview = ', '.join(label_stats['common_labels_removed'][:5]) + if len(label_stats['common_labels_removed']) > 5: + removed_preview += "..." + report_lines.append(f"- Common removed labels: {removed_preview}") + if label_stats['common_labels_added']: + added_preview = ', '.join(label_stats['common_labels_added'][:5]) + if len(label_stats['common_labels_added']) > 5: + added_preview += "..." + report_lines.append(f"- Common added labels: {added_preview}") + report_lines.append("") + + # Type changes summary + type_changes = diff_data['type_changes'] + if type_changes: + report_lines.append("## Type Changes Summary") + report_lines.append(f"- Metrics with type changes: {len(type_changes)}") + for metric, change in list(type_changes.items())[:5]: # Show first 5 + report_lines.append(f" - {metric}: {change['original']} β†’ {change['generated']}") + if len(type_changes) > 5: + report_lines.append(f" - ... and {len(type_changes) - 5} more") + report_lines.append("") + + # Removed metrics + if diff_data['removed_metrics']: + report_lines.append("## Removed Metrics") + for metric in diff_data['removed_metrics']: + section = diff_tool.original[metric].section if metric in diff_tool.original else "Unknown" + report_lines.append(f"- {metric} (from {section})") + report_lines.append("") + + # Added metrics + if diff_data['added_metrics']: + report_lines.append("## Added Metrics") + for metric in diff_data['added_metrics']: + section = diff_tool.generated[metric].section if metric in diff_tool.generated else "Unknown" + report_lines.append(f"- {metric} (in {section})") + report_lines.append("") + + # Modified metrics (show top 10) + if diff_data['modified_metrics']: + report_lines.append("## Modified Metrics (Top 10)") + count = 0 + for metric_name, changes in diff_data['modified_metrics'].items(): + if count >= 10: + report_lines.append(f"... and {len(diff_data['modified_metrics']) - 10} more modified metrics") + break + + report_lines.append(f"### {metric_name}") + + if 'description' in changes: + desc_change = changes['description'] + report_lines.append("**Description changed:**") + if len(desc_change['original']) > 100 or len(desc_change['generated']) > 100: + report_lines.append(f"- Length change: {desc_change['length_diff']:+d} characters") + report_lines.append(f"- Original: {desc_change['original'][:100]}...") + report_lines.append(f"- Generated: {desc_change['generated'][:100]}...") + else: + report_lines.append(f"- Original: {desc_change['original']}") + report_lines.append(f"- Generated: {desc_change['generated']}") + report_lines.append("") + + if 'type' in changes: + report_lines.append("**Type changed:**") + report_lines.append(f"- {changes['type']['original']} β†’ {changes['type']['generated']}") + report_lines.append("") + + if 'labels' in changes: + label_changes = changes['labels'] + report_lines.append("**Labels changed:**") + report_lines.append(f"- Count: {label_changes['original_count']} β†’ {label_changes['generated_count']}") + if label_changes['removed']: + report_lines.append(f"- Removed: {', '.join(label_changes['removed'])}") + if label_changes['added']: + report_lines.append(f"- Added: {', '.join(label_changes['added'])}") + report_lines.append("") + + if 'usage' in changes: + usage_change = changes['usage'] + report_lines.append("**Usage changed:**") + report_lines.append(f"- Had usage: {usage_change['original_has_usage']} β†’ {usage_change['generated_has_usage']}") + report_lines.append("") + + if 'section' in changes: + report_lines.append("**Section changed:**") + report_lines.append(f"- {changes['section']['original']} β†’ {changes['section']['generated']}") + + report_lines.append("---") + report_lines.append("") + count += 1 + + # Section changes + if diff_data['section_changes']: + report_lines.append("## Section Changes") + for section, changes in diff_data['section_changes'].items(): + report_lines.append(f"### {section}") + report_lines.append(f"- Metric count: {changes['original_count']} β†’ {changes['generated_count']}") + if changes['removed']: + removed_preview = ', '.join(changes['removed'][:5]) + if len(changes['removed']) > 5: + removed_preview += f" ... ({len(changes['removed']) - 5} more)" + report_lines.append(f"- Removed: {removed_preview}") + if changes['added']: + added_preview = ', '.join(changes['added'][:5]) + if len(changes['added']) > 5: + added_preview += f" ... ({len(changes['added']) - 5} more)" + report_lines.append(f"- Added: {added_preview}") + if changes['moved_out']: + report_lines.append(f"- Moved out: {', '.join(changes['moved_out'][:3])}...") + if changes['moved_in']: + report_lines.append(f"- Moved in: {', '.join(changes['moved_in'][:3])}...") + report_lines.append("") + + return '\n'.join(report_lines) + + +def main(): + """Main function to run the metrics diff tool""" + parser = argparse.ArgumentParser( + description='Compare Redpanda metrics documentation files (public and/or internal)', + epilog=''' +Examples: + %(prog)s --original-public orig_pub.adoc --generated-public gen_pub.adoc + %(prog)s --original-internal orig_int.adoc --generated-internal gen_int.adoc + %(prog)s --original-public orig_pub.adoc --generated-public gen_pub.adoc --original-internal orig_int.adoc --generated-internal gen_int.adoc + ''', + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + # Public metrics arguments + parser.add_argument('--original-public', help='Path to original (published) public metrics file') + parser.add_argument('--generated-public', help='Path to generated (automated) public metrics file') + + # Internal metrics arguments + parser.add_argument('--original-internal', help='Path to original (published) internal metrics file') + parser.add_argument('--generated-internal', help='Path to generated (automated) internal metrics file') + + # Output arguments + parser.add_argument('--output', help='Output file for the combined report (default: metrics_diff_report.md)') + parser.add_argument('--json', help='Output file for JSON data (default: metrics_diff_data.json)') + parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output') + parser.add_argument('--debug', action='store_true', help='Debug parsing (shows first 10 metrics found)') + + args = parser.parse_args() + + # Validate arguments + if not any([args.original_public, args.original_internal]): + print("Error: You must specify at least one type of metrics to compare.") + print("Use --original-public and --generated-public for public metrics,") + print("or --original-internal and --generated-internal for internal metrics,") + print("or both.") + sys.exit(1) + + if args.original_public and not args.generated_public: + print("Error: --original-public requires --generated-public") + sys.exit(1) + + if args.original_internal and not args.generated_internal: + print("Error: --original-internal requires --generated-internal") + sys.exit(1) + + # Initialize diff tools + public_diff = None + internal_diff = None + + # Process public metrics if provided + if args.original_public and args.generated_public: + try: + print(f"Loading public metrics files...") + print(f" Original: {args.original_public}") + with open(args.original_public, 'r', encoding='utf-8') as f: + orig_public_content = f.read() + + print(f" Generated: {args.generated_public}") + with open(args.generated_public, 'r', encoding='utf-8') as f: + gen_public_content = f.read() + + print("Parsing public metrics...") + orig_public_parser = MetricsParser("===") # Public metrics use === + gen_public_parser = MetricsParser("===") + + orig_public_metrics = orig_public_parser.parse_file(orig_public_content) + gen_public_metrics = gen_public_parser.parse_file(gen_public_content) + + print(f"βœ“ Parsed {len(orig_public_metrics)} original public metrics") + print(f"βœ“ Parsed {len(gen_public_metrics)} generated public metrics") + + public_diff = MetricsDiff(orig_public_metrics, gen_public_metrics, "public") + + except FileNotFoundError as e: + print(f"Error: Could not find public metrics file '{e.filename}'") + sys.exit(1) + except Exception as e: + print(f"Error processing public metrics: {e}") + sys.exit(1) + + # Process internal metrics if provided + if args.original_internal and args.generated_internal: + try: + print(f"Loading internal metrics files...") + print(f" Original: {args.original_internal}") + with open(args.original_internal, 'r', encoding='utf-8') as f: + orig_internal_content = f.read() + + print(f" Generated: {args.generated_internal}") + with open(args.generated_internal, 'r', encoding='utf-8') as f: + gen_internal_content = f.read() + + print("Parsing internal metrics...") + orig_internal_parser = MetricsParser("==") # Internal metrics use == + gen_internal_parser = MetricsParser("==") + + orig_internal_metrics = orig_internal_parser.parse_file(orig_internal_content) + gen_internal_metrics = gen_internal_parser.parse_file(gen_internal_content) + + print(f"βœ“ Parsed {len(orig_internal_metrics)} original internal metrics") + print(f"βœ“ Parsed {len(gen_internal_metrics)} generated internal metrics") + + if args.debug: + print("\nDEBUG: First 10 original internal metrics found:") + for i, (name, metric) in enumerate(list(orig_internal_metrics.items())[:10]): + print(f" {i+1:2d}. {name} (type: {metric.metric_type}, section: {metric.section})") + + print("\nDEBUG: First 10 generated internal metrics found:") + for i, (name, metric) in enumerate(list(gen_internal_metrics.items())[:10]): + print(f" {i+1:2d}. {name} (type: {metric.metric_type}, section: {metric.section})") + + internal_diff = MetricsDiff(orig_internal_metrics, gen_internal_metrics, "internal") + + except FileNotFoundError as e: + print(f"Error: Could not find internal metrics file '{e.filename}'") + sys.exit(1) + except Exception as e: + print(f"Error processing internal metrics: {e}") + sys.exit(1) + + # Verbose output + if args.verbose: + if public_diff: + print("\nPublic metrics summary:") + pub_data = public_diff.compare_metrics() + print(f" Original: {pub_data['summary']['total_original']}") + print(f" Generated: {pub_data['summary']['total_generated']}") + print(f" Changes: {pub_data['summary']['modified_count']} modified, {pub_data['summary']['added_count']} added, {pub_data['summary']['removed_count']} removed") + + if internal_diff: + print("\nInternal metrics summary:") + int_data = internal_diff.compare_metrics() + print(f" Original: {int_data['summary']['total_original']}") + print(f" Generated: {int_data['summary']['total_generated']}") + print(f" Changes: {int_data['summary']['modified_count']} modified, {int_data['summary']['added_count']} added, {int_data['summary']['removed_count']} removed") + + # Generate reports + print("\nAnalyzing differences...") + report_generator = DualMetricsReportGenerator(public_diff, internal_diff) + + report_file = args.output or "metrics_diff_report.md" + json_file = args.json or "metrics_diff_data.json" + + try: + # Generate combined report + report = report_generator.generate_combined_report(report_file) + + print("\n" + "="*60) + print("METRICS DOCUMENTATION DIFF REPORT") + print("="*60) + print(report[:3000]) # Show first 3000 characters + if len(report) > 3000: + print(f"\n... (truncated, full report saved to {report_file})") + + # Save detailed JSON data + combined_data = {} + if public_diff: + combined_data['public'] = public_diff.compare_metrics() + if internal_diff: + combined_data['internal'] = internal_diff.compare_metrics() + + with open(json_file, "w", encoding='utf-8') as f: + json.dump(combined_data, f, indent=2, default=str) + + print(f"\nβœ“ Combined report saved to: {report_file}") + print(f"βœ“ JSON data saved to: {json_file}") + + # Summary stats + total_original = 0 + total_generated = 0 + total_changes = 0 + + if public_diff: + pub_data = public_diff.compare_metrics() + total_original += pub_data['summary']['total_original'] + total_generated += pub_data['summary']['total_generated'] + total_changes += pub_data['summary']['modified_count'] + pub_data['summary']['added_count'] + pub_data['summary']['removed_count'] + + if internal_diff: + int_data = internal_diff.compare_metrics() + total_original += int_data['summary']['total_original'] + total_generated += int_data['summary']['total_generated'] + total_changes += int_data['summary']['modified_count'] + int_data['summary']['added_count'] + int_data['summary']['removed_count'] + + print(f"\nπŸ“Š Overall Summary:") + print(f" Total metrics: {total_original} β†’ {total_generated} ({total_generated - total_original:+d})") + print(f" Total changes: {total_changes}") + + if public_diff and internal_diff: + print(f" Public metrics processed: βœ“") + print(f" Internal metrics processed: βœ“") + elif public_diff: + print(f" Public metrics processed: βœ“") + elif internal_diff: + print(f" Internal metrics processed: βœ“") + + except Exception as e: + print(f"Error generating report: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file