Created
November 6, 2025 18:27
-
-
Save jasonnerothin/e5f4a9844067ed9717131b7d97c6924e to your computer and use it in GitHub Desktop.
stack agnostic http trace provider
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| HTTPS Tracer - Distributed tracing for HTTPS client/server communication | |
| Client-side: Uses standard requests library (no framework dependency) | |
| Server-side: Provides generic patterns adaptable to any HTTP framework | |
| """ | |
| from typing import Optional, Dict, Any, Callable | |
| import requests | |
| from opentelemetry import trace | |
| from opentelemetry.trace import SpanKind | |
| from opentelemetry.propagate import inject, extract | |
| from common.threadsafe_otlp_traces import ( | |
| get_tracer, | |
| set_http_client_attributes, | |
| set_http_server_attributes, | |
| record_event, | |
| record_exception, | |
| set_span_error | |
| ) | |
| # ============================================================================ | |
| # CLIENT SIDE - HTTP Requests with Trace Context | |
| # ============================================================================ | |
| def make_traced_request( | |
| tracer: trace.Tracer, | |
| method: str, | |
| url: str, | |
| span_name: Optional[str] = None, | |
| peer_service: Optional[str] = None, | |
| json: Optional[Dict[str, Any]] = None, | |
| data: Optional[Any] = None, | |
| headers: Optional[Dict[str, str]] = None, | |
| timeout: int = 30, | |
| **kwargs | |
| ) -> requests.Response: | |
| """ | |
| Make an HTTP request with trace context propagation. | |
| Creates a CLIENT span and injects trace context into HTTP headers. | |
| Args: | |
| tracer: OpenTelemetry tracer instance | |
| method: HTTP method (GET, POST, etc.) | |
| url: Target URL | |
| span_name: Optional span name (default: "{method} {url}") | |
| peer_service: Target service name (for service map) | |
| json: JSON payload | |
| data: Request body data | |
| headers: Additional HTTP headers | |
| timeout: Request timeout in seconds | |
| **kwargs: Additional arguments passed to requests.request() | |
| Returns: | |
| Response object | |
| Raises: | |
| requests.RequestException: If request fails | |
| Example: | |
| from common.https_tracer import make_traced_request | |
| response = make_traced_request( | |
| tracer=tracer, | |
| method="POST", | |
| url="https://ares:8443/pray", | |
| peer_service="ares", | |
| json={"status": "ready"}, | |
| span_name="send_prayer" | |
| ) | |
| """ | |
| span_name = span_name or f"{method} {url}" | |
| with tracer.start_as_current_span( | |
| span_name, | |
| kind=SpanKind.CLIENT | |
| ) as span: | |
| try: | |
| # Set HTTP client attributes | |
| set_http_client_attributes( | |
| span, | |
| method=method, | |
| url=url, | |
| peer_service=peer_service | |
| ) | |
| # Prepare headers with trace context | |
| request_headers = headers.copy() if headers else {} | |
| inject(request_headers) | |
| record_event(span, "request_started", { | |
| "method": method, | |
| "url": url | |
| }) | |
| # Make the request | |
| response = requests.request( | |
| method=method, | |
| url=url, | |
| json=json, | |
| data=data, | |
| headers=request_headers, | |
| timeout=timeout, | |
| **kwargs | |
| ) | |
| # Set response status code | |
| span.set_attribute("http.status_code", response.status_code) | |
| span.set_attribute("http.response.status_code", response.status_code) | |
| # Mark as error if status code >= 400 | |
| if response.status_code >= 400: | |
| set_span_error(span, f"HTTP {response.status_code}") | |
| record_event(span, "request_completed", { | |
| "status_code": response.status_code, | |
| "response_size": len(response.content) | |
| }) | |
| return response | |
| except Exception as e: | |
| record_exception(span, e) | |
| raise | |
| class TracedHTTPClient: | |
| """ | |
| HTTP client wrapper with automatic tracing. | |
| Provides a simple interface for making traced HTTP requests. | |
| """ | |
| def __init__( | |
| self, | |
| tracer: trace.Tracer, | |
| service_name: str, | |
| default_timeout: int = 30 | |
| ): | |
| """ | |
| Initialize traced HTTP client. | |
| Args: | |
| tracer: OpenTelemetry tracer instance | |
| service_name: Name of the service making requests | |
| default_timeout: Default request timeout | |
| """ | |
| self.tracer = tracer | |
| self.service_name = service_name | |
| self.default_timeout = default_timeout | |
| def get(self, url: str, peer_service: Optional[str] = None, **kwargs) -> requests.Response: | |
| """Make a traced GET request.""" | |
| return make_traced_request( | |
| tracer=self.tracer, | |
| method="GET", | |
| url=url, | |
| peer_service=peer_service, | |
| timeout=kwargs.pop('timeout', self.default_timeout), | |
| **kwargs | |
| ) | |
| def post( | |
| self, | |
| url: str, | |
| peer_service: Optional[str] = None, | |
| json: Optional[Dict[str, Any]] = None, | |
| **kwargs | |
| ) -> requests.Response: | |
| """Make a traced POST request.""" | |
| return make_traced_request( | |
| tracer=self.tracer, | |
| method="POST", | |
| url=url, | |
| peer_service=peer_service, | |
| json=json, | |
| timeout=kwargs.pop('timeout', self.default_timeout), | |
| **kwargs | |
| ) | |
| def put( | |
| self, | |
| url: str, | |
| peer_service: Optional[str] = None, | |
| json: Optional[Dict[str, Any]] = None, | |
| **kwargs | |
| ) -> requests.Response: | |
| """Make a traced PUT request.""" | |
| return make_traced_request( | |
| tracer=self.tracer, | |
| method="PUT", | |
| url=url, | |
| peer_service=peer_service, | |
| json=json, | |
| timeout=kwargs.pop('timeout', self.default_timeout), | |
| **kwargs | |
| ) | |
| def delete(self, url: str, peer_service: Optional[str] = None, **kwargs) -> requests.Response: | |
| """Make a traced DELETE request.""" | |
| return make_traced_request( | |
| tracer=self.tracer, | |
| method="DELETE", | |
| url=url, | |
| peer_service=peer_service, | |
| timeout=kwargs.pop('timeout', self.default_timeout), | |
| **kwargs | |
| ) | |
| # ============================================================================ | |
| # SERVER SIDE - Generic Trace Context Extraction | |
| # ============================================================================ | |
| def extract_trace_context_from_headers(headers: Dict[str, str]) -> Optional[trace.Context]: | |
| """ | |
| Extract trace context from HTTP headers (framework-agnostic). | |
| Args: | |
| headers: HTTP request headers as dict | |
| Returns: | |
| OpenTelemetry Context object, or None if extraction fails | |
| Example: | |
| # With any HTTP framework, extract headers as dict: | |
| headers = { | |
| 'traceparent': '00-abc123...', | |
| 'content-type': 'application/json', | |
| # ... other headers | |
| } | |
| ctx = extract_trace_context_from_headers(headers) | |
| with tracer.start_as_current_span("handler", context=ctx, kind=SpanKind.SERVER): | |
| # Handle request | |
| pass | |
| """ | |
| try: | |
| # OpenTelemetry extract works with dict-like objects | |
| ctx = extract(headers) | |
| return ctx | |
| except Exception: | |
| return None | |
| def create_server_span( | |
| tracer: trace.Tracer, | |
| span_name: str, | |
| method: str, | |
| path: str, | |
| headers: Dict[str, str], | |
| extract_context: bool = True | |
| ): | |
| """ | |
| Create a SERVER span with trace context extraction (framework-agnostic). | |
| Use this as a context manager in your HTTP handler. | |
| Args: | |
| tracer: OpenTelemetry tracer instance | |
| span_name: Name for the span | |
| method: HTTP method (GET, POST, etc.) | |
| path: Request path | |
| headers: Request headers as dict | |
| extract_context: If True, extract parent context from headers | |
| Returns: | |
| Context manager yielding the span | |
| Example: | |
| # In your HTTP handler: | |
| def handle_request(method, path, headers, body): | |
| with create_server_span( | |
| tracer, | |
| "handle_pray", | |
| method, | |
| path, | |
| headers | |
| ) as span: | |
| # Process request | |
| result = process_pray(body) | |
| # Set response status | |
| span.set_attribute("http.status_code", 200) | |
| return result | |
| """ | |
| # Extract context if requested | |
| context = None | |
| if extract_context: | |
| context = extract_trace_context_from_headers(headers) | |
| # Return context manager | |
| return tracer.start_as_current_span( | |
| span_name, | |
| context=context, | |
| kind=SpanKind.SERVER | |
| ) | |
| class ServerSpanHelper: | |
| """ | |
| Helper class for managing server spans with proper attributes. | |
| Use this in your HTTP server handler to create properly configured spans. | |
| """ | |
| def __init__( | |
| self, | |
| tracer: trace.Tracer, | |
| span_name: str, | |
| method: str, | |
| path: str, | |
| headers: Dict[str, str] | |
| ): | |
| """ | |
| Initialize server span helper. | |
| Args: | |
| tracer: OpenTelemetry tracer instance | |
| span_name: Name for the span | |
| method: HTTP method | |
| path: Request path | |
| headers: Request headers as dict | |
| """ | |
| self.tracer = tracer | |
| self.span_name = span_name | |
| self.method = method | |
| self.path = path | |
| self.headers = headers | |
| self.span = None | |
| def __enter__(self): | |
| """Start the span.""" | |
| # Extract context | |
| context = extract_trace_context_from_headers(self.headers) | |
| # Create span | |
| self.span = self.tracer.start_span( | |
| self.span_name, | |
| context=context, | |
| kind=SpanKind.SERVER | |
| ) | |
| # Set initial attributes | |
| set_http_server_attributes( | |
| self.span, | |
| method=self.method, | |
| route=self.path, | |
| status_code=200 # Default, update later | |
| ) | |
| record_event(self.span, "request_received", { | |
| "method": self.method, | |
| "path": self.path | |
| }) | |
| return self.span | |
| def __exit__(self, exc_type, exc_val, exc_tb): | |
| """End the span.""" | |
| if self.span: | |
| if exc_val: | |
| record_exception(self.span, exc_val) | |
| self.span.set_attribute("http.status_code", 500) | |
| record_event(self.span, "request_completed") | |
| self.span.end() | |
| # ============================================================================ | |
| # USAGE EXAMPLES FOR DIFFERENT SCENARIOS | |
| # ============================================================================ | |
| """ | |
| EXAMPLE 1: Simple HTTP Server Handler (Generic) | |
| ------------------------------------------------ | |
| def handle_pray_request(method, path, headers, body): | |
| '''Handle incoming /pray request''' | |
| with create_server_span( | |
| tracer, | |
| "handle_prayer", | |
| method, | |
| path, | |
| headers | |
| ) as span: | |
| try: | |
| # Parse request | |
| data = json.loads(body) | |
| # Process | |
| result = process_prayer(data) | |
| # Set success status | |
| span.set_attribute("http.status_code", 200) | |
| return 200, json.dumps(result) | |
| except Exception as e: | |
| record_exception(span, e) | |
| span.set_attribute("http.status_code", 500) | |
| return 500, json.dumps({"error": str(e)}) | |
| EXAMPLE 2: With http.server (Python standard library) | |
| ------------------------------------------------------ | |
| from http.server import BaseHTTPRequestHandler, HTTPServer | |
| from common.https_tracer import extract_trace_context_from_headers | |
| class TracedHTTPHandler(BaseHTTPRequestHandler): | |
| def do_POST(self): | |
| # Extract headers | |
| headers = dict(self.headers) | |
| # Create server span | |
| with create_server_span( | |
| tracer, | |
| f"POST {self.path}", | |
| "POST", | |
| self.path, | |
| headers | |
| ) as span: | |
| try: | |
| # Read body | |
| content_length = int(self.headers.get('Content-Length', 0)) | |
| body = self.rfile.read(content_length) | |
| # Process request | |
| result = handle_request(body) | |
| # Send response | |
| self.send_response(200) | |
| self.send_header('Content-Type', 'application/json') | |
| self.end_headers() | |
| self.wfile.write(json.dumps(result).encode()) | |
| span.set_attribute("http.status_code", 200) | |
| except Exception as e: | |
| record_exception(span, e) | |
| self.send_error(500, str(e)) | |
| EXAMPLE 3: With Flask (Optional - if you add Flask later) | |
| ---------------------------------------------------------- | |
| # Save this as common/flask_tracer.py if you add Flask later | |
| from flask import Flask, request | |
| from functools import wraps | |
| def traced_route(tracer, span_name=None): | |
| def decorator(func): | |
| @wraps(func) | |
| def wrapper(*args, **kwargs): | |
| headers = dict(request.headers) | |
| with create_server_span( | |
| tracer, | |
| span_name or func.__name__, | |
| request.method, | |
| request.path, | |
| headers | |
| ) as span: | |
| result = func(*args, **kwargs) | |
| # Extract status code if tuple returned | |
| status_code = 200 | |
| if isinstance(result, tuple) and len(result) >= 2: | |
| status_code = result[1] | |
| span.set_attribute("http.status_code", status_code) | |
| return result | |
| return wrapper | |
| return decorator | |
| # Usage: | |
| # @app.route('/pray', methods=['POST']) | |
| # @traced_route(tracer, "handle_prayer") | |
| # def pray(): | |
| # return {"status": "ok"}, 200 | |
| EXAMPLE 4: With aiohttp (Async) | |
| -------------------------------- | |
| from aiohttp import web | |
| from common.https_tracer import extract_trace_context_from_headers | |
| async def handle_pray(request): | |
| # Extract headers | |
| headers = dict(request.headers) | |
| # Create span | |
| with create_server_span( | |
| tracer, | |
| "handle_prayer", | |
| request.method, | |
| request.path, | |
| headers | |
| ) as span: | |
| try: | |
| data = await request.json() | |
| result = await process_prayer(data) | |
| span.set_attribute("http.status_code", 200) | |
| return web.json_response(result) | |
| except Exception as e: | |
| record_exception(span, e) | |
| span.set_attribute("http.status_code", 500) | |
| return web.json_response({"error": str(e)}, status=500) | |
| EXAMPLE 5: Manual Span Management (Full Control) | |
| ------------------------------------------------- | |
| def handle_request(method, path, headers, body): | |
| # Extract context | |
| ctx = extract_trace_context_from_headers(headers) | |
| # Start span manually | |
| span = tracer.start_span( | |
| "handle_request", | |
| context=ctx, | |
| kind=SpanKind.SERVER | |
| ) | |
| try: | |
| # Set attributes | |
| set_http_server_attributes(span, method, path, 200) | |
| # Process | |
| result = process_request(body) | |
| # Success | |
| record_event(span, "request_completed") | |
| return 200, result | |
| except Exception as e: | |
| record_exception(span, e) | |
| span.set_attribute("http.status_code", 500) | |
| return 500, {"error": str(e)} | |
| finally: | |
| span.end() | |
| """ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment