126 lines
4.1 KiB
Python
126 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Any, Optional
|
|
|
|
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 OpenTelemetry.
|
|
"""
|
|
|
|
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,
|
|
}
|
|
|
|
start = time.perf_counter()
|
|
try:
|
|
response = requests.get(url, timeout=self.timeout_seconds)
|
|
attrs["http_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"
|
|
return response.text
|
|
except Exception as exc:
|
|
attrs["outcome"] = "error"
|
|
attrs.setdefault("error_type", exc.__class__.__name__)
|
|
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_ms = (time.perf_counter() - start) * 1000.0
|
|
self._telemetry.request_counter.add(1, attributes=attrs)
|
|
self._telemetry.duration_histogram.record(duration_ms, attributes=attrs)
|
|
if attrs.get("outcome") == "error":
|
|
self._telemetry.error_counter.add(1, attributes=attrs)
|