File size: 7,002 Bytes
447ebeb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""
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()