""" SpanAttributes Value Usage Checker This script ensures that all SpanAttributes enum references in the OpenTelemetry integration are properly accessed with the .value property. This is important because: 1. Without .value, the enum object itself is used instead of its string value 2. This can cause type errors or unexpected behavior in OpenTelemetry exporters 3. It's a consistent pattern that should be followed for all enum usage Example of correct usage: span.set_attribute(key=SpanAttributes.LLM_USER.value, value="user123") Example of incorrect usage: span.set_attribute(key=SpanAttributes.LLM_USER, value="user123") The script checks both through AST parsing (for accurate code analysis) and regex (for backup coverage) to find any violations. Usage: python tests/code_coverage_tests/check_spanattributes_value_usage.py python tests/code_coverage_tests/check_spanattributes_value_usage.py --debug """ import argparse import ast import os import re from typing import List, Tuple import sys # Add parent directory to path so we can import litellm sys.path.insert(0, os.path.abspath("../..")) import litellm class SpanAttributesUsageChecker(ast.NodeVisitor): """ Checks if SpanAttributes is used without .value when setting attributes in safe_set_attribute calls and other attribute setting methods in opentelemetry.py. This is important to ensure consistent enum value access and prevent type errors when sending data to OpenTelemetry exporters. """ def __init__(self, debug=False): self.violations = [] self.debug = debug def visit_Call(self, node): # Check if this is a call to safe_set_attribute or set_attribute if isinstance(node.func, ast.Attribute) and node.func.attr in ['safe_set_attribute', 'set_attribute']: # Look for the 'key' parameter for keyword in node.keywords: if keyword.arg == 'key': # Check if the value is a SpanAttributes member without .value if isinstance(keyword.value, ast.Attribute) and \ isinstance(keyword.value.value, ast.Name) and \ keyword.value.value.id == 'SpanAttributes': # Get the source code for this attribute try: attr_source = ast.unparse(keyword.value) if not attr_source.endswith('.value'): if self.debug: print(f"AST found violation: {node.lineno}: {attr_source}") self.violations.append((node.lineno, f"{attr_source} used without .value")) except AttributeError: # For Python < 3.9, ast.unparse doesn't exist # Fallback to our best guess if keyword.value.attr != 'value' and not hasattr(keyword.value, 'value'): violation_msg = f"SpanAttributes.{keyword.value.attr} used without .value" if self.debug: print(f"AST found violation: {node.lineno}: {violation_msg}") self.violations.append((node.lineno, violation_msg)) # Continue the visit self.generic_visit(node) def check_file(file_path: str, debug: bool = False) -> List[Tuple[int, str]]: """ Analyze a Python file to check for SpanAttributes usage without .value Args: file_path: Path to the Python file to check debug: Whether to print debug information Returns: List of (line_number, message) tuples identifying violations """ with open(file_path, 'r') as file: content = file.read() # First try AST parsing for accurate code structure analysis try: tree = ast.parse(content) checker = SpanAttributesUsageChecker(debug=debug) checker.visit(tree) violations = checker.violations # Also do a regex check for backup/extra coverage # This catches cases that might be missed by AST parsing # Split content into lines for more precise analysis lines = content.splitlines() for i, line in enumerate(lines, 1): # Skip lines that contain ".value" after "SpanAttributes." # This prevents false positives for correct usage if re.search(r"SpanAttributes\.[A-Z_][A-Z0-9_]*\.value", line): if debug: print(f"Line {i} skipped - contains .value: {line.strip()}") continue # Pattern: Looking for "key=SpanAttributes.ENUM_NAME" without .value at the end pattern = r"key\s*=\s*SpanAttributes\.[A-Z_][A-Z0-9_]*(?!\.value)" match = re.search(pattern, line) if match: # Check if this violation was already found by AST if not any(i == line_num for line_num, _ in violations): if debug: print(f"Regex found violation: {i}: {match.group(0)}") violations.append((i, f"SpanAttributes used without .value: {match.group(0)}")) return violations except SyntaxError: print(f"Syntax error in {file_path}") return [] def main(): """ Main function to run the SpanAttributes usage check on the OpenTelemetry integration file. Exits with code 1 if violations are found, 0 otherwise. """ parser = argparse.ArgumentParser(description='Check for SpanAttributes used without .value') parser.add_argument('--debug', action='store_true', help='Enable debug output') args = parser.parse_args() # Path to the OpenTelemetry integration file target_file = os.path.join("litellm", "integrations", "opentelemetry.py") if not os.path.exists(target_file): # Try alternate path for local development target_file = os.path.join("..", "..", "litellm", "integrations", "opentelemetry.py") if not os.path.exists(target_file): print(f"Error: Could not find file at {target_file}") exit(1) violations = check_file(target_file, debug=args.debug) if violations: print(f"Found {len(violations)} SpanAttributes without .value in {target_file}:") # Sort violations by line number for better readability violations.sort(key=lambda x: x[0]) for line, message in violations: print(f" Line {line}: {message}") print("\nDirect enum reference can cause errors. Always use .value with SpanAttributes enums.") exit(1) else: print(f"All SpanAttributes are used correctly with .value in {target_file}") exit(0) if __name__ == "__main__": main()