Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

260 lines
9.9 KiB
Python

"""Lightweight in-process OTLP collector for tracing tests.
Provides a minimal OTLP collector that receives traces via gRPC (with HTTP
fallback) and stores them in memory for test assertions, eliminating the need
for Docker-based opentelemetry-collector and file I/O.
Usage::
collector = LightweightOtlpCollector(port=4317)
collector.start()
# ... run code that emits traces ...
assert collector.has_span("my_span")
collector.stop()
"""
import json
import logging
import threading
from concurrent import futures
from dataclasses import dataclass, field
from typing import Any, Dict, List, Set
logger = logging.getLogger(__name__)
@dataclass
class Span:
"""Represents a single span extracted from OTLP trace data."""
name: str
trace_id: str = ""
span_id: str = ""
parent_span_id: str = ""
start_time_ns: int = 0
end_time_ns: int = 0
attributes: Dict[str, Any] = field(default_factory=dict)
events: List[Dict[str, Any]] = field(default_factory=list)
class LightweightOtlpCollector:
"""A minimal OTLP collector that stores traces in memory for test assertions.
This replaces the Docker-based opentelemetry-collector for testing purposes.
It listens on a gRPC port for OTLP trace data and stores spans in memory,
allowing tests to verify specific spans based on trace level.
"""
def __init__(self, port: int = 4317):
self.port = port
self._server = None
self._thread = None
self._running = False
self._lock = threading.Lock()
# In-memory storage for collected spans
self._spans: List[Span] = []
self._raw_traces: List[Dict[str, Any]] = []
def _try_grpc_server(self):
"""Try to start gRPC server with full OTLP protocol."""
try:
from grpc import server as grpc_server
from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import (
ExportTraceServiceResponse,
)
from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import (
TraceServiceServicer,
add_TraceServiceServicer_to_server,
)
class TraceServicer(TraceServiceServicer):
def __init__(self, collector):
self.collector = collector
def Export(self, request, context):
self.collector._handle_trace_request(request)
return ExportTraceServiceResponse()
self._server = grpc_server(futures.ThreadPoolExecutor(max_workers=4))
add_TraceServiceServicer_to_server(TraceServicer(self), self._server)
self._server.add_insecure_port(f"127.0.0.1:{self.port}")
return True
except ImportError:
logger.warning("Full gRPC OTLP not available, using HTTP fallback")
return False
def _handle_trace_request(self, request):
"""Handle incoming trace request and extract spans to memory."""
with self._lock:
try:
trace_data = self._protobuf_to_dict(request)
self._raw_traces.append(trace_data)
# Extract spans from the trace data
self._extract_spans(trace_data)
except Exception as e:
logger.error(f"Failed to process trace: {e}")
def _protobuf_to_dict(self, proto_obj) -> Dict[str, Any]:
"""Convert protobuf message to dict."""
result = {}
for field, value in proto_obj.ListFields():
if field.message_type:
type_name = type(value).__name__
if "Repeated" in type_name:
result[field.name] = [self._protobuf_to_dict(v) for v in value]
else:
result[field.name] = self._protobuf_to_dict(value)
else:
result[field.name] = value
return result
def _extract_spans(self, trace_data: Dict[str, Any]):
"""Extract Span objects from OTLP trace data structure."""
resource_spans = trace_data.get("resource_spans", [])
for rs in resource_spans:
scope_spans = rs.get("scope_spans", [])
for ss in scope_spans:
spans = ss.get("spans", [])
for span_data in spans:
span = Span(
name=span_data.get("name", ""),
trace_id=span_data.get("trace_id", ""),
span_id=span_data.get("span_id", ""),
parent_span_id=span_data.get("parent_span_id", ""),
start_time_ns=span_data.get("start_time_unix_nano", 0),
end_time_ns=span_data.get("end_time_unix_nano", 0),
attributes=span_data.get("attributes", {}),
events=span_data.get("events", []),
)
self._spans.append(span)
def _http_server_loop(self):
"""Fallback HTTP server for OTLP HTTP protocol."""
from http.server import BaseHTTPRequestHandler, HTTPServer
class OTLPHandler(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server):
self.collector = server.collector
super().__init__(request, client_address, server)
def do_POST(self):
if self.path in ["/v1/traces", "/v1/traces/"]:
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length)
try:
data = json.loads(body)
with self.collector._lock:
self.collector._raw_traces.append(data)
self.collector._extract_spans_http(data)
self.send_response(200)
self.end_headers()
except Exception as e:
logger.error(f"HTTP trace handling error: {e}")
self.send_response(500)
self.end_headers()
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
pass # Suppress HTTP server logs
class CollectorHTTPServer(HTTPServer):
def __init__(self, server_address, collector):
self.collector = collector
super().__init__(
server_address,
lambda r, a, s: OTLPHandler(r, a, s),
)
server = CollectorHTTPServer(("127.0.0.1", 4318), self)
server.timeout = 0.5
while self._running:
server.handle_request()
def _extract_spans_http(self, data: Dict[str, Any]):
"""Extract Span objects from OTLP HTTP JSON format."""
resource_spans = data.get("resourceSpans", [])
for rs in resource_spans:
scope_spans = rs.get("scopeSpans", [])
for ss in scope_spans:
spans = ss.get("spans", [])
for span_data in spans:
span = Span(
name=span_data.get("name", ""),
trace_id=span_data.get("traceId", ""),
span_id=span_data.get("spanId", ""),
parent_span_id=span_data.get("parentSpanId", ""),
start_time_ns=span_data.get("startTimeUnixNano", 0),
end_time_ns=span_data.get("endTimeUnixNano", 0),
attributes=span_data.get("attributes", {}),
events=span_data.get("events", []),
)
self._spans.append(span)
def start(self):
"""Start the collector server."""
self._running = True
self._spans.clear()
self._raw_traces.clear()
if self._try_grpc_server():
self._server.start()
logger.info(f"OTLP gRPC collector started on port {self.port}")
else:
# Fallback to HTTP server in a thread
self._thread = threading.Thread(target=self._http_server_loop, daemon=True)
self._thread.start()
logger.info("OTLP HTTP collector started on port 4318")
def stop(self):
"""Stop the collector server."""
self._running = False
if self._server:
self._server.stop(1)
self._server = None
logger.info("OTLP collector stopped")
# ========================================================================
# Public API for test assertions
# ========================================================================
def get_spans(self) -> List[Span]:
"""Get all collected spans."""
with self._lock:
return list(self._spans)
def get_span_names(self) -> Set[str]:
"""Get all unique span names."""
with self._lock:
return {s.name for s in self._spans}
def has_span(self, name: str) -> bool:
"""Check if a span with the given name exists."""
return name in self.get_span_names()
def has_any_span(self, names: List[str]) -> bool:
"""Check if any of the given span names exist."""
span_names = self.get_span_names()
return any(name in span_names for name in names)
def has_all_spans(self, names: List[str]) -> bool:
"""Check if all of the given span names exist."""
span_names = self.get_span_names()
return all(name in span_names for name in names)
def get_spans_by_name(self, name: str) -> List[Span]:
"""Get all spans with the given name."""
with self._lock:
return [s for s in self._spans if s.name == name]
def count_spans(self) -> int:
"""Get total count of collected spans."""
with self._lock:
return len(self._spans)
def clear(self):
"""Clear all collected spans."""
with self._lock:
self._spans.clear()
self._raw_traces.clear()