mirror of
https://github.com/tiennm99/litellm.git
synced 2026-06-17 22:48:35 +00:00
196 lines
7.2 KiB
Python
196 lines
7.2 KiB
Python
"""
|
|
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()
|