Files
starry-sdk-observability-demo/starry-sdk/starry_client_sdk/client.py
2026-05-06 14:06:44 +08:00

149 lines
5.3 KiB
Python

from __future__ import annotations
import time
from dataclasses import dataclass
from typing import Any, Optional
from opentelemetry.trace import SpanKind, Status, StatusCode
import requests
from .telemetry import SDK_NAME, configure_telemetry
from .version import __version__
DEFAULT_BASE_URL = "https://blog.starryskymeow.top"
class StarrySdkError(Exception):
"""Base exception for this demo SDK."""
class StarryNotFoundError(StarrySdkError):
"""Raised when the upstream service returns HTTP 404."""
def __init__(self, *, path: str, url: str) -> None:
self.path = path
self.url = url
self.status_code = 404
super().__init__(f"Resource not found: path={path!r}, url={url!r}")
@dataclass(frozen=True)
class _CallContext:
path: str
url: str
sdk_interface: str = "get"
http_method: str = "GET"
def _normalize_path(path: str) -> str:
if path is None:
raise ValueError("path must be a string, got None")
return str(path).lstrip("/")
def _metric_path(path: str) -> str:
# Keep metrics low-cardinality: drop query strings and collapse an empty path to "/".
normalized = _normalize_path(path).split("?", 1)[0]
return f"/{normalized}" if normalized else "/"
def _build_url(base_url: str, path: str) -> str:
normalized = _normalize_path(path)
if normalized:
return f"{base_url.rstrip('/')}/{normalized}"
return f"{base_url.rstrip('/')}/"
class StarryClient:
"""Demo client SDK.
`get(path)` requests `https://blog.starryskymeow.top/{path}` and returns `str`.
HTTP 404 is converted to `StarryNotFoundError` and logged through SDK telemetry.
"""
def __init__(
self,
*,
base_url: str = DEFAULT_BASE_URL,
timeout_seconds: float = 10.0,
sdk_version: str = __version__,
service_name: Optional[str] = None,
) -> None:
self.base_url = base_url.rstrip("/")
self.timeout_seconds = timeout_seconds
self.sdk_version = sdk_version
self._telemetry = configure_telemetry(service_name=service_name, sdk_version=sdk_version)
def get(self, path: str = "") -> str:
normalized_path = _normalize_path(path)
url = _build_url(self.base_url, normalized_path)
context = _CallContext(path=normalized_path, url=url)
attrs: dict[str, Any] = {
"sdk_name": SDK_NAME,
"sdk_version": self.sdk_version,
"sdk_interface": context.sdk_interface,
"http_method": context.http_method,
"url_path": _metric_path(normalized_path),
"outcome": "unknown",
"http_status_code": 0,
"error_type": "none",
}
with self._telemetry.tracer.start_as_current_span(
"starry_client_sdk.get",
kind=SpanKind.CLIENT,
attributes={
"sdk.name": SDK_NAME,
"sdk.version": self.sdk_version,
"sdk.interface": context.sdk_interface,
"http.request.method": context.http_method,
"url.path": attrs["url_path"],
},
record_exception=False,
set_status_on_exception=False,
) as span:
start = time.perf_counter()
try:
response = requests.get(url, timeout=self.timeout_seconds)
attrs["http_status_code"] = response.status_code
span.set_attribute("http.response.status_code", response.status_code)
if response.status_code == 404:
attrs["outcome"] = "error"
attrs["error_type"] = "StarryNotFoundError"
raise StarryNotFoundError(path=normalized_path, url=url)
response.raise_for_status()
attrs["outcome"] = "success"
span.set_attribute("sdk.outcome", attrs["outcome"])
span.set_status(Status(StatusCode.OK))
return response.text
except Exception as exc:
attrs["outcome"] = "error"
attrs["error_type"] = exc.__class__.__name__
span.set_attribute("sdk.outcome", attrs["outcome"])
span.set_attribute("error.type", attrs["error_type"])
span.record_exception(exc)
span.set_status(Status(StatusCode.ERROR, attrs["error_type"]))
self._telemetry.logger.exception(
"Starry SDK request error",
extra={
"sdk_name": SDK_NAME,
"sdk_version": self.sdk_version,
"sdk_interface": context.sdk_interface,
"http_method": context.http_method,
"http_status_code": attrs.get("http_status_code", 0),
"url_path": attrs["url_path"],
"error_type": attrs["error_type"],
},
)
raise
finally:
duration_seconds = time.perf_counter() - start
span.set_attribute("sdk.duration_seconds", duration_seconds)
self._telemetry.request_counter.add(1, attributes=attrs)
self._telemetry.duration_histogram.record(duration_seconds, attributes=attrs)
if attrs.get("outcome") == "error":
self._telemetry.error_counter.add(1, attributes=attrs)