demo with sdk and backend
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
||||||
15
demo-app/Dockerfile
Normal file
15
demo-app/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM docker.1ms.run/python:3.12-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY starry-sdk /app/starry-sdk
|
||||||
|
COPY demo-app/requirements.txt /app/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r /app/requirements.txt && \
|
||||||
|
pip install --no-cache-dir -e /app/starry-sdk
|
||||||
|
|
||||||
|
COPY demo-app/main.py /app/main.py
|
||||||
|
|
||||||
|
CMD ["python", "/app/main.py"]
|
||||||
46
demo-app/main.py
Normal file
46
demo-app/main.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from starry_client_sdk import StarryClient, StarryNotFoundError, force_flush
|
||||||
|
|
||||||
|
|
||||||
|
def _bool_env(name: str, default: bool) -> bool:
|
||||||
|
raw = os.getenv(name)
|
||||||
|
if raw is None:
|
||||||
|
return default
|
||||||
|
return raw.strip().lower() not in {"0", "false", "f", "no", "n", "off"}
|
||||||
|
|
||||||
|
|
||||||
|
def _paths() -> list[str]:
|
||||||
|
raw = os.getenv("DEMO_PATHS", ",__sdk_demo_not_found__")
|
||||||
|
return [item.strip() for item in raw.split(",")]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
client = StarryClient()
|
||||||
|
loop = _bool_env("DEMO_LOOP", True)
|
||||||
|
interval_seconds = float(os.getenv("DEMO_INTERVAL_SECONDS", "5"))
|
||||||
|
paths = _paths()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
for path in paths:
|
||||||
|
try:
|
||||||
|
text = client.get(path)
|
||||||
|
print(f"OK path={path!r} bytes={len(text)}")
|
||||||
|
except StarryNotFoundError as exc:
|
||||||
|
print(f"NOT_FOUND path={path!r} error={exc}")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"ERROR path={path!r} type={exc.__class__.__name__} error={exc}")
|
||||||
|
|
||||||
|
# Demo app is short-looping, so flush to make logs/metrics visible quickly.
|
||||||
|
force_flush()
|
||||||
|
|
||||||
|
if not loop:
|
||||||
|
break
|
||||||
|
time.sleep(interval_seconds)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
demo-app/requirements.txt
Normal file
1
demo-app/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# SDK dependencies are declared in starry-sdk/pyproject.toml.
|
||||||
123
docker-compose.yml
Normal file
123
docker-compose.yml
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
services:
|
||||||
|
fluentbit:
|
||||||
|
depends_on:
|
||||||
|
vlagent:
|
||||||
|
condition: service_started
|
||||||
|
required: true
|
||||||
|
image: cr.fluentbit.io/fluent/fluent-bit:3.1.7
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
default: null
|
||||||
|
ports:
|
||||||
|
- 4318:4318
|
||||||
|
volumes:
|
||||||
|
- ./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
|
||||||
|
victorialogs-1:
|
||||||
|
command:
|
||||||
|
- -storageDataPath=/vlogs
|
||||||
|
- -loggerFormat=json
|
||||||
|
- -datadog.streamFields=service,hostname,ddsource
|
||||||
|
- -journald.streamFields=_HOSTNAME,_SYSTEMD_UNIT,_PID
|
||||||
|
- -journald.ignoreFields=MESSAGE_ID,INVOCATION_ID,USER_INVOCATION_ID
|
||||||
|
- -journald.ignoreFields=_BOOT_ID,_MACHINE_ID,_SYSTEMD_INVOCATION_ID,_STREAM_ID,_UID
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- wget
|
||||||
|
- -qO-
|
||||||
|
- http://127.0.0.1:9428/health
|
||||||
|
timeout: 1s
|
||||||
|
interval: 1s
|
||||||
|
retries: 10
|
||||||
|
image: docker.io/victoriametrics/victoria-logs:v1.50.0
|
||||||
|
networks:
|
||||||
|
default: null
|
||||||
|
ports:
|
||||||
|
- mode: ingress
|
||||||
|
target: 9428
|
||||||
|
published: "9428"
|
||||||
|
protocol: tcp
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: victorialogs-1
|
||||||
|
target: /vlogs
|
||||||
|
volume: {}
|
||||||
|
victoriametrics:
|
||||||
|
command:
|
||||||
|
- -storageDataPath=/vmsingle
|
||||||
|
- -loggerFormat=json
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- wget
|
||||||
|
- -qO-
|
||||||
|
- http://127.0.0.1:8428/health
|
||||||
|
timeout: 1s
|
||||||
|
interval: 1s
|
||||||
|
retries: 10
|
||||||
|
image: victoriametrics/victoria-metrics:v1.132.0
|
||||||
|
networks:
|
||||||
|
default: null
|
||||||
|
ports:
|
||||||
|
- 8428:8428
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: victoriametrics
|
||||||
|
target: /vmsingle
|
||||||
|
volume: {}
|
||||||
|
vlagent:
|
||||||
|
command:
|
||||||
|
- --remoteWrite.tmpDataPath=/vlagent
|
||||||
|
- --remoteWrite.url=http://victorialogs-1:9428/insert/native
|
||||||
|
- --syslog.listenAddr.tcp=0.0.0.0:8094
|
||||||
|
depends_on:
|
||||||
|
victorialogs-1:
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- wget
|
||||||
|
- -qO-
|
||||||
|
- http://127.0.0.1:9429/health
|
||||||
|
timeout: 1s
|
||||||
|
interval: 1s
|
||||||
|
retries: 10
|
||||||
|
image: victoriametrics/vlagent:v1.50.0
|
||||||
|
networks:
|
||||||
|
default: null
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: vlagent
|
||||||
|
target: /vlagent
|
||||||
|
volume: {}
|
||||||
|
demo-app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: demo-app/Dockerfile
|
||||||
|
environment:
|
||||||
|
STARRYSDK_TELEMETRY_ENABLED: "true"
|
||||||
|
STARRYSDK_SERVICE_NAME: starry-python-sdk-demo
|
||||||
|
STARRYSDK_METRIC_EXPORT_INTERVAL_MS: "5000"
|
||||||
|
#OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
|
||||||
|
DEMO_LOOP: "true"
|
||||||
|
DEMO_INTERVAL_SECONDS: "5"
|
||||||
|
DEMO_PATHS: ",__sdk_demo_not_found__"
|
||||||
|
DEFAULT_OTLP_ENDPOINT: "http://fluentbit:4318"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: fluentbit-oltp_default
|
||||||
|
volumes:
|
||||||
|
victorialogs-1:
|
||||||
|
name: fluentbit-oltp_victorialogs-1
|
||||||
|
external: true
|
||||||
|
victoriametrics:
|
||||||
|
name: fluentbit-oltp_victoriametrics
|
||||||
|
external: true
|
||||||
|
vlagent:
|
||||||
|
name: fluentbit-oltp_vlagent
|
||||||
|
external: true
|
||||||
69
fluent-bit.conf
Normal file
69
fluent-bit.conf
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
[SERVICE]
|
||||||
|
Flush 1
|
||||||
|
Log_Level info
|
||||||
|
Parsers_File parsers.conf
|
||||||
|
|
||||||
|
# 方便调试 Fluent Bit 自身状态;不要对公网暴露
|
||||||
|
HTTP_Server On
|
||||||
|
HTTP_Listen 0.0.0.0
|
||||||
|
HTTP_Port 2020
|
||||||
|
|
||||||
|
# 可选:给 Fluent Bit 文件缓冲目录,配合 compose 里的 volume
|
||||||
|
storage.path /buffers
|
||||||
|
storage.sync normal
|
||||||
|
storage.checksum off
|
||||||
|
storage.backlog.mem_limit 64M
|
||||||
|
|
||||||
|
# Python SDK / OpenTelemetry SDK 发到这里:
|
||||||
|
# http://vmauth:8427/v1/logs
|
||||||
|
# http://vmauth:8427/v1/metrics
|
||||||
|
# vmauth 会转发到 fluentbit:4318
|
||||||
|
[INPUT]
|
||||||
|
Name opentelemetry
|
||||||
|
Listen 0.0.0.0
|
||||||
|
Port 4318
|
||||||
|
|
||||||
|
# OTLP/HTTP 通常期望 200;Fluent Bit 默认是 201
|
||||||
|
Successful_Response_Code 200
|
||||||
|
|
||||||
|
# 保持默认行为:/v1/logs -> v1_logs, /v1/metrics -> v1_metrics
|
||||||
|
Tag_From_Uri true
|
||||||
|
|
||||||
|
Buffer_Chunk_Size 1M
|
||||||
|
Buffer_Max_Size 10M
|
||||||
|
Threaded On
|
||||||
|
|
||||||
|
# Python SDK 发来的 metrics -> VictoriaMetrics remote_write
|
||||||
|
[OUTPUT]
|
||||||
|
Name prometheus_remote_write
|
||||||
|
Match *
|
||||||
|
Host victoriametrics
|
||||||
|
Port 8428
|
||||||
|
Uri /api/v1/write
|
||||||
|
|
||||||
|
# 可选公共 label,便于区分来源
|
||||||
|
Add_Label otel_pipeline fluent-bit
|
||||||
|
Workers 2
|
||||||
|
|
||||||
|
# Python SDK 发来的 logs -> vlagent -> VictoriaLogs
|
||||||
|
[OUTPUT]
|
||||||
|
Name opentelemetry
|
||||||
|
Match *
|
||||||
|
Host vlagent
|
||||||
|
Port 9429
|
||||||
|
|
||||||
|
Logs_Uri /insert/opentelemetry/v1/logs
|
||||||
|
|
||||||
|
# VictoriaLogs 摄取日志时的字段映射。
|
||||||
|
# Python OTel log body 通常能被自动处理;这里保留多个候选字段更稳。
|
||||||
|
Header VL-Msg-Field body,Body,message,msg,log,_msg
|
||||||
|
|
||||||
|
# 只把低基数字段作为 stream fields,避免 path、status_code 这类字段爆炸。
|
||||||
|
Header VL-Stream-Fields service.name,service.namespace,service.version,deployment.environment,host.name
|
||||||
|
|
||||||
|
Compress gzip
|
||||||
|
Workers 2
|
||||||
|
|
||||||
|
[OUTPUT]
|
||||||
|
Name stdout
|
||||||
|
Match *
|
||||||
12
starry-sdk/README.md
Normal file
12
starry-sdk/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# starry-client-sdk demo
|
||||||
|
|
||||||
|
Demo SDK: request `https://blog.starryskymeow.top/{path}`, return response text, and raise `StarryNotFoundError` on HTTP 404. Telemetry is enabled by default and exported with OTLP/HTTP.
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
|
||||||
|
- `STARRYSDK_TELEMETRY_ENABLED`: default `true`; set `false` to disable SDK telemetry.
|
||||||
|
- `STARRYSDK_SERVICE_NAME`: default `starry-python-sdk-consumer`.
|
||||||
|
- `STARRYSDK_METRIC_EXPORT_INTERVAL_MS`: default `5000`.
|
||||||
|
- `OTEL_EXPORTER_OTLP_ENDPOINT`: default `http://localhost:4318`; SDK appends `/v1/metrics` and `/v1/logs` for OTLP/HTTP.
|
||||||
|
- `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`: optional explicit metrics endpoint.
|
||||||
|
- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`: optional explicit logs endpoint.
|
||||||
21
starry-sdk/pyproject.toml
Normal file
21
starry-sdk/pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "starry-client-sdk"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Demo Python SDK with default-on OpenTelemetry metrics and exception logs."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = [
|
||||||
|
"requests>=2.32.0,<3.0",
|
||||||
|
"opentelemetry-api==1.41.1",
|
||||||
|
"opentelemetry-sdk==1.41.1",
|
||||||
|
"opentelemetry-exporter-otlp-proto-http==1.41.1",
|
||||||
|
"opentelemetry-instrumentation-logging==0.62b1"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["starry_client_sdk*"]
|
||||||
11
starry-sdk/starry_client_sdk/__init__.py
Normal file
11
starry-sdk/starry_client_sdk/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from .client import StarryClient, StarryNotFoundError, StarrySdkError
|
||||||
|
from .telemetry import force_flush
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"StarryClient",
|
||||||
|
"StarrySdkError",
|
||||||
|
"StarryNotFoundError",
|
||||||
|
"force_flush",
|
||||||
|
"__version__",
|
||||||
|
]
|
||||||
125
starry-sdk/starry_client_sdk/client.py
Normal file
125
starry-sdk/starry_client_sdk/client.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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)
|
||||||
178
starry-sdk/starry_client_sdk/telemetry.py
Normal file
178
starry-sdk/starry_client_sdk/telemetry.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
SDK_NAME = "starry-client-sdk"
|
||||||
|
DEFAULT_SERVICE_NAME = "starry-python-sdk-consumer"
|
||||||
|
DEFAULT_OTLP_ENDPOINT = "http://host.docker.internal:4318"
|
||||||
|
|
||||||
|
_LOCK = threading.Lock()
|
||||||
|
_HANDLES: Optional["TelemetryHandles"] = None
|
||||||
|
_METER_PROVIDER: Any = None
|
||||||
|
_LOGGER_PROVIDER: Any = None
|
||||||
|
|
||||||
|
|
||||||
|
class _NoopCounter:
|
||||||
|
def add(self, amount: int | float, attributes: Optional[dict[str, Any]] = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _NoopHistogram:
|
||||||
|
def record(self, amount: int | float, attributes: Optional[dict[str, Any]] = None) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TelemetryHandles:
|
||||||
|
request_counter: Any
|
||||||
|
error_counter: Any
|
||||||
|
duration_histogram: Any
|
||||||
|
logger: logging.Logger
|
||||||
|
enabled: bool
|
||||||
|
|
||||||
|
|
||||||
|
def _truthy(value: str) -> bool:
|
||||||
|
return value.strip().lower() not in {"0", "false", "f", "no", "n", "off", "disabled"}
|
||||||
|
|
||||||
|
|
||||||
|
def telemetry_enabled() -> bool:
|
||||||
|
return _truthy(os.getenv("STARRYSDK_TELEMETRY_ENABLED", "true"))
|
||||||
|
|
||||||
|
|
||||||
|
def _base_endpoint() -> str:
|
||||||
|
return os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", DEFAULT_OTLP_ENDPOINT).rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def _metrics_endpoint() -> str:
|
||||||
|
return os.getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", f"{_base_endpoint()}/v1/metrics")
|
||||||
|
|
||||||
|
|
||||||
|
def _logs_endpoint() -> str:
|
||||||
|
return os.getenv("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", f"{_base_endpoint()}/v1/logs")
|
||||||
|
|
||||||
|
|
||||||
|
def _noop_handles() -> TelemetryHandles:
|
||||||
|
return TelemetryHandles(
|
||||||
|
request_counter=_NoopCounter(),
|
||||||
|
error_counter=_NoopCounter(),
|
||||||
|
duration_histogram=_NoopHistogram(),
|
||||||
|
logger=logging.getLogger("starry_client_sdk"),
|
||||||
|
enabled=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_telemetry(*, service_name: Optional[str] = None, sdk_version: str = __version__) -> TelemetryHandles:
|
||||||
|
"""Configure default-on, non-blocking telemetry for this SDK.
|
||||||
|
|
||||||
|
This demo intentionally configures local OpenTelemetry providers owned by the SDK, so the SDK can
|
||||||
|
be observable by default without overwriting an application's global OpenTelemetry configuration.
|
||||||
|
Export failures must never break business calls; if setup fails, the SDK falls back to no-op meters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
global _HANDLES, _METER_PROVIDER, _LOGGER_PROVIDER
|
||||||
|
|
||||||
|
with _LOCK:
|
||||||
|
if _HANDLES is not None:
|
||||||
|
return _HANDLES
|
||||||
|
|
||||||
|
logger = logging.getLogger("starry_client_sdk")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
if not telemetry_enabled():
|
||||||
|
_HANDLES = _noop_handles()
|
||||||
|
return _HANDLES
|
||||||
|
|
||||||
|
try:
|
||||||
|
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
||||||
|
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
||||||
|
from opentelemetry.instrumentation.logging.handler import LoggingHandler
|
||||||
|
from opentelemetry.sdk._logs import LoggerProvider
|
||||||
|
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
||||||
|
from opentelemetry.sdk.metrics import MeterProvider
|
||||||
|
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
||||||
|
from opentelemetry.sdk.resources import Resource
|
||||||
|
|
||||||
|
service_name = service_name or os.getenv("STARRYSDK_SERVICE_NAME", DEFAULT_SERVICE_NAME)
|
||||||
|
export_interval_ms = int(os.getenv("STARRYSDK_METRIC_EXPORT_INTERVAL_MS", "5000"))
|
||||||
|
|
||||||
|
resource = Resource.create(
|
||||||
|
{
|
||||||
|
"service.name": service_name,
|
||||||
|
"sdk.name": SDK_NAME,
|
||||||
|
"sdk.version": sdk_version,
|
||||||
|
"telemetry.source": "client-sdk",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
metric_exporter = OTLPMetricExporter(endpoint=_metrics_endpoint())
|
||||||
|
metric_reader = PeriodicExportingMetricReader(
|
||||||
|
metric_exporter,
|
||||||
|
export_interval_millis=export_interval_ms,
|
||||||
|
)
|
||||||
|
_METER_PROVIDER = MeterProvider(resource=resource, metric_readers=[metric_reader])
|
||||||
|
meter = _METER_PROVIDER.get_meter(SDK_NAME, sdk_version)
|
||||||
|
|
||||||
|
_LOGGER_PROVIDER = LoggerProvider(resource=resource)
|
||||||
|
log_exporter = OTLPLogExporter(endpoint=_logs_endpoint())
|
||||||
|
_LOGGER_PROVIDER.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
|
||||||
|
|
||||||
|
# Attach only one OTLP handler to the SDK logger. Do not attach to root logger.
|
||||||
|
if not any(getattr(handler, "_starry_sdk_otel_handler", False) for handler in logger.handlers):
|
||||||
|
otel_handler = LoggingHandler(level=logging.INFO, logger_provider=_LOGGER_PROVIDER)
|
||||||
|
setattr(otel_handler, "_starry_sdk_otel_handler", True)
|
||||||
|
logger.addHandler(otel_handler)
|
||||||
|
|
||||||
|
_HANDLES = TelemetryHandles(
|
||||||
|
request_counter=meter.create_counter(
|
||||||
|
"starry.sdk.client.requests",
|
||||||
|
unit="1",
|
||||||
|
description="Total SDK client calls.",
|
||||||
|
),
|
||||||
|
error_counter=meter.create_counter(
|
||||||
|
"starry.sdk.client.errors",
|
||||||
|
unit="1",
|
||||||
|
description="Total SDK client calls ending in an exception.",
|
||||||
|
),
|
||||||
|
duration_histogram=meter.create_histogram(
|
||||||
|
"starry.sdk.client.request.duration.ms",
|
||||||
|
unit="ms",
|
||||||
|
description="SDK client call latency in milliseconds.",
|
||||||
|
),
|
||||||
|
logger=logger,
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
|
atexit.register(shutdown_telemetry)
|
||||||
|
return _HANDLES
|
||||||
|
except Exception: # Telemetry must not break SDK business behavior.
|
||||||
|
logging.getLogger("starry_client_sdk.telemetry").debug("SDK telemetry setup failed", exc_info=True)
|
||||||
|
_HANDLES = _noop_handles()
|
||||||
|
return _HANDLES
|
||||||
|
|
||||||
|
|
||||||
|
def force_flush(timeout_millis: int = 5000) -> None:
|
||||||
|
"""Flush telemetry buffers. Useful in short-lived CLI/demo processes."""
|
||||||
|
|
||||||
|
for provider in (_METER_PROVIDER, _LOGGER_PROVIDER):
|
||||||
|
if provider is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
provider.force_flush(timeout_millis=timeout_millis)
|
||||||
|
except Exception:
|
||||||
|
logging.getLogger("starry_client_sdk.telemetry").debug("Telemetry force_flush failed", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown_telemetry() -> None:
|
||||||
|
for provider in (_METER_PROVIDER, _LOGGER_PROVIDER):
|
||||||
|
if provider is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
provider.shutdown()
|
||||||
|
except Exception:
|
||||||
|
logging.getLogger("starry_client_sdk.telemetry").debug("Telemetry shutdown failed", exc_info=True)
|
||||||
1
starry-sdk/starry_client_sdk/version.py
Normal file
1
starry-sdk/starry_client_sdk/version.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.1.0"
|
||||||
Reference in New Issue
Block a user