instrumentation-sdk 1.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- instrumentation_sdk-1.7.0/PKG-INFO +18 -0
- instrumentation_sdk-1.7.0/pyproject.toml +34 -0
- instrumentation_sdk-1.7.0/setup.cfg +4 -0
- instrumentation_sdk-1.7.0/src/__init__.py +39 -0
- instrumentation_sdk-1.7.0/src/api/rest/v1/app.py +16 -0
- instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/deterministic_sampling.py +30 -0
- instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/instrumentation.py +73 -0
- instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/metrics.py +104 -0
- instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/minilm_embedding.py +32 -0
- instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/pii_injection.py +33 -0
- instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/streaming.py +55 -0
- instrumentation_sdk-1.7.0/src/api/rest/v1/handlers/token_counting.py +31 -0
- instrumentation_sdk-1.7.0/src/api/rest/v1/router.py +18 -0
- instrumentation_sdk-1.7.0/src/features/__init__.py +0 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/__init__.py +18 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/domain/__init__.py +0 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/domain/mappers.py +71 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/index.py +70 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/__init__.py +0 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/__init__.py +0 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/anthropic_patcher.py +86 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/base.py +10 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/http_patcher.py +127 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/langchain_patcher.py +96 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/litellm_patcher.py +88 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/infra/patchers/openai_patcher.py +88 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/ports.py +15 -0
- instrumentation_sdk-1.7.0/src/features/auto_instrumentation/service.py +34 -0
- instrumentation_sdk-1.7.0/src/features/deterministic_sampling/__init__.py +3 -0
- instrumentation_sdk-1.7.0/src/features/deterministic_sampling/index.py +9 -0
- instrumentation_sdk-1.7.0/src/features/deterministic_sampling/infra/adapters/sha256_sampling_adapter.py +21 -0
- instrumentation_sdk-1.7.0/src/features/deterministic_sampling/ports.py +5 -0
- instrumentation_sdk-1.7.0/src/features/deterministic_sampling/service.py +9 -0
- instrumentation_sdk-1.7.0/src/features/manual_instrumentation/__init__.py +3 -0
- instrumentation_sdk-1.7.0/src/features/manual_instrumentation/index.py +3 -0
- instrumentation_sdk-1.7.0/src/features/manual_instrumentation/service.py +141 -0
- instrumentation_sdk-1.7.0/src/features/manual_instrumentation/tests/unit/test_context.py +129 -0
- instrumentation_sdk-1.7.0/src/features/metrics/__init__.py +1 -0
- instrumentation_sdk-1.7.0/src/features/metrics/index.py +49 -0
- instrumentation_sdk-1.7.0/src/features/metrics/infra/__init__.py +1 -0
- instrumentation_sdk-1.7.0/src/features/metrics/infra/adapters/__init__.py +1 -0
- instrumentation_sdk-1.7.0/src/features/metrics/infra/adapters/prometheus_adapter.py +79 -0
- instrumentation_sdk-1.7.0/src/features/metrics/ports.py +27 -0
- instrumentation_sdk-1.7.0/src/features/metrics/service.py +105 -0
- instrumentation_sdk-1.7.0/src/features/minilm_embedding/__init__.py +3 -0
- instrumentation_sdk-1.7.0/src/features/minilm_embedding/index.py +67 -0
- instrumentation_sdk-1.7.0/src/features/minilm_embedding/infra/adapters/http_embedding_client_adapter.py +18 -0
- instrumentation_sdk-1.7.0/src/features/minilm_embedding/ports.py +5 -0
- instrumentation_sdk-1.7.0/src/features/minilm_embedding/service.py +12 -0
- instrumentation_sdk-1.7.0/src/features/pii_injection_scan/__init__.py +1 -0
- instrumentation_sdk-1.7.0/src/features/pii_injection_scan/index.py +9 -0
- instrumentation_sdk-1.7.0/src/features/pii_injection_scan/infra/adapters/aho_corasick.py +48 -0
- instrumentation_sdk-1.7.0/src/features/pii_injection_scan/infra/adapters/aho_corasick_scanner_adapter.py +92 -0
- instrumentation_sdk-1.7.0/src/features/pii_injection_scan/ports.py +5 -0
- instrumentation_sdk-1.7.0/src/features/pii_injection_scan/service.py +9 -0
- instrumentation_sdk-1.7.0/src/features/spans/__init__.py +1 -0
- instrumentation_sdk-1.7.0/src/features/spans/decorator.py +70 -0
- instrumentation_sdk-1.7.0/src/features/spans/globals.py +18 -0
- instrumentation_sdk-1.7.0/src/features/spans/index.py +5 -0
- instrumentation_sdk-1.7.0/src/features/spans/reporter.py +10 -0
- instrumentation_sdk-1.7.0/src/features/spans/tests/__init__.py +0 -0
- instrumentation_sdk-1.7.0/src/features/spans/tests/unit/__init__.py +0 -0
- instrumentation_sdk-1.7.0/src/features/spans/tests/unit/test_decorator.py +53 -0
- instrumentation_sdk-1.7.0/src/features/spans/tests/unit/test_decorator_edge_cases.py +125 -0
- instrumentation_sdk-1.7.0/src/features/spans/types.py +118 -0
- instrumentation_sdk-1.7.0/src/features/streaming/__init__.py +3 -0
- instrumentation_sdk-1.7.0/src/features/streaming/index.py +25 -0
- instrumentation_sdk-1.7.0/src/features/streaming/infra/adapters/token_counter_adapter.py +7 -0
- instrumentation_sdk-1.7.0/src/features/streaming/ports.py +19 -0
- instrumentation_sdk-1.7.0/src/features/streaming/service.py +227 -0
- instrumentation_sdk-1.7.0/src/features/token_counting/__init__.py +3 -0
- instrumentation_sdk-1.7.0/src/features/token_counting/index.py +11 -0
- instrumentation_sdk-1.7.0/src/features/token_counting/infra/__init__.py +1 -0
- instrumentation_sdk-1.7.0/src/features/token_counting/infra/adapters/__init__.py +1 -0
- instrumentation_sdk-1.7.0/src/features/token_counting/infra/adapters/tiktoken_adapter.py +10 -0
- instrumentation_sdk-1.7.0/src/features/token_counting/ports.py +8 -0
- instrumentation_sdk-1.7.0/src/features/token_counting/service.py +173 -0
- instrumentation_sdk-1.7.0/src/infra/__init__.py +0 -0
- instrumentation_sdk-1.7.0/src/infra/adapters/__init__.py +0 -0
- instrumentation_sdk-1.7.0/src/infra/adapters/kafka/__init__.py +0 -0
- instrumentation_sdk-1.7.0/src/infra/adapters/kafka/adapter.py +24 -0
- instrumentation_sdk-1.7.0/src/infra/clients/__init__.py +0 -0
- instrumentation_sdk-1.7.0/src/infra/clients/v1/llm/observability/v1/instrumentation_pb2.py +89 -0
- instrumentation_sdk-1.7.0/src/infra/clients/v1/llm/observability/v1/instrumentation_pb2_grpc.py +498 -0
- instrumentation_sdk-1.7.0/src/infra/clients/v1/llm/observability/v1/span_pb2.py +39 -0
- instrumentation_sdk-1.7.0/src/infra/clients/v1/llm/observability/v1/span_pb2_grpc.py +69 -0
- instrumentation_sdk-1.7.0/src/infra/metrics/__init__.py +1 -0
- instrumentation_sdk-1.7.0/src/infra/metrics/meter.py +27 -0
- instrumentation_sdk-1.7.0/src/infra/tracing/middleware.py +16 -0
- instrumentation_sdk-1.7.0/src/infra/tracing/tracer.py +35 -0
- instrumentation_sdk-1.7.0/src/instrumentation_sdk.egg-info/PKG-INFO +18 -0
- instrumentation_sdk-1.7.0/src/instrumentation_sdk.egg-info/SOURCES.txt +93 -0
- instrumentation_sdk-1.7.0/src/instrumentation_sdk.egg-info/dependency_links.txt +1 -0
- instrumentation_sdk-1.7.0/src/instrumentation_sdk.egg-info/requires.txt +13 -0
- instrumentation_sdk-1.7.0/src/instrumentation_sdk.egg-info/top_level.txt +5 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: instrumentation-sdk
|
|
3
|
+
Version: 1.7.0
|
|
4
|
+
Summary: LLM Observability Instrumentation SDK
|
|
5
|
+
Author: LLM Observability Team
|
|
6
|
+
Requires-Dist: pydantic>=2.0.0
|
|
7
|
+
Requires-Dist: tiktoken>=0.5.0
|
|
8
|
+
Requires-Dist: PyYAML>=6.0.0
|
|
9
|
+
Requires-Dist: confluent-kafka>=2.3.0
|
|
10
|
+
Requires-Dist: protobuf>=4.25.0
|
|
11
|
+
Requires-Dist: fastapi>=0.110.0
|
|
12
|
+
Requires-Dist: httpx>=0.27.0
|
|
13
|
+
Requires-Dist: opentelemetry-api>=1.24.0
|
|
14
|
+
Requires-Dist: opentelemetry-sdk>=1.24.0
|
|
15
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.24.0
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.45b0
|
|
17
|
+
Requires-Dist: opentelemetry-exporter-prometheus>=0.45b0
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.23.0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "instrumentation-sdk"
|
|
3
|
+
version = "1.7.0"
|
|
4
|
+
description = "LLM Observability Instrumentation SDK"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "LLM Observability Team" }
|
|
7
|
+
]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"pydantic>=2.0.0",
|
|
10
|
+
"tiktoken>=0.5.0",
|
|
11
|
+
"PyYAML>=6.0.0",
|
|
12
|
+
"confluent-kafka>=2.3.0",
|
|
13
|
+
"protobuf>=4.25.0",
|
|
14
|
+
"fastapi>=0.110.0",
|
|
15
|
+
"httpx>=0.27.0",
|
|
16
|
+
"opentelemetry-api>=1.24.0",
|
|
17
|
+
"opentelemetry-sdk>=1.24.0",
|
|
18
|
+
"opentelemetry-exporter-otlp>=1.24.0",
|
|
19
|
+
"opentelemetry-instrumentation-fastapi>=0.45b0",
|
|
20
|
+
"opentelemetry-exporter-prometheus>=0.45b0",
|
|
21
|
+
"pytest-asyncio>=0.23.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["setuptools>=61.0"]
|
|
27
|
+
build-backend = "setuptools.build_meta"
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
testpaths = ["tests"]
|
|
31
|
+
pythonpath = ["src"]
|
|
32
|
+
markers = [
|
|
33
|
+
"performance: marks tests as load/performance tests (deselect with '-m not performance')",
|
|
34
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from .features.spans import llm_observe, SpanReporter
|
|
2
|
+
from .features.manual_instrumentation import llm_span
|
|
3
|
+
from .features.spans.globals import set_reporter, get_reporter
|
|
4
|
+
from .features.auto_instrumentation import (
|
|
5
|
+
init_auto_instrumentation,
|
|
6
|
+
uninstrument_all,
|
|
7
|
+
instrument_client,
|
|
8
|
+
instrument_http_client,
|
|
9
|
+
detect_llm_call,
|
|
10
|
+
trigger_test_call
|
|
11
|
+
)
|
|
12
|
+
from .features.token_counting import count_tokens, llm_span_with_tokens
|
|
13
|
+
from .features.streaming import llm_streaming_span, wrap_stream, wrap_async_stream
|
|
14
|
+
from .features.pii_injection_scan import scan_prompt
|
|
15
|
+
from .features.metrics import init_metrics_pipeline, record_span_metrics
|
|
16
|
+
from .features.deterministic_sampling import should_sample
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"llm_observe",
|
|
20
|
+
"llm_span",
|
|
21
|
+
"set_reporter",
|
|
22
|
+
"get_reporter",
|
|
23
|
+
"SpanReporter",
|
|
24
|
+
"init_auto_instrumentation",
|
|
25
|
+
"uninstrument_all",
|
|
26
|
+
"instrument_client",
|
|
27
|
+
"instrument_http_client",
|
|
28
|
+
"detect_llm_call",
|
|
29
|
+
"trigger_test_call",
|
|
30
|
+
"count_tokens",
|
|
31
|
+
"llm_span_with_tokens",
|
|
32
|
+
"llm_streaming_span",
|
|
33
|
+
"wrap_stream",
|
|
34
|
+
"wrap_async_stream",
|
|
35
|
+
"scan_prompt",
|
|
36
|
+
"init_metrics_pipeline",
|
|
37
|
+
"record_span_metrics",
|
|
38
|
+
"should_sample",
|
|
39
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from .router import api_v1_router
|
|
5
|
+
from ....infra.tracing.middleware import instrument_app
|
|
6
|
+
|
|
7
|
+
def create_app() -> FastAPI:
|
|
8
|
+
app = FastAPI(title="Instrumentation SDK API", version="1.0.0")
|
|
9
|
+
app.include_router(api_v1_router, prefix="/v1")
|
|
10
|
+
instrument_app(app)
|
|
11
|
+
return app
|
|
12
|
+
|
|
13
|
+
# Always define the app instance for ASGI servers like uvicorn
|
|
14
|
+
app = create_app()
|
|
15
|
+
|
|
16
|
+
# Build cache test
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
import os
|
|
4
|
+
from opentelemetry import trace
|
|
5
|
+
from .....features.deterministic_sampling.index import should_sample
|
|
6
|
+
|
|
7
|
+
router = APIRouter(prefix="/sampling", tags=["Deterministic Sampling"])
|
|
8
|
+
|
|
9
|
+
class SamplingGateRequest(BaseModel):
|
|
10
|
+
span_id: str
|
|
11
|
+
|
|
12
|
+
class SamplingGateResponse(BaseModel):
|
|
13
|
+
is_sampled: bool
|
|
14
|
+
|
|
15
|
+
def _set_span_attributes() -> None:
|
|
16
|
+
span = trace.get_current_span()
|
|
17
|
+
span.set_attribute("service.name", "instrumentation-sdk-api")
|
|
18
|
+
span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
|
|
19
|
+
span.set_attribute("feature.name", "deterministic_sampling")
|
|
20
|
+
|
|
21
|
+
@router.post("/should-sample", response_model=SamplingGateResponse)
|
|
22
|
+
def should_sample_endpoint(request: SamplingGateRequest) -> SamplingGateResponse:
|
|
23
|
+
_set_span_attributes()
|
|
24
|
+
try:
|
|
25
|
+
sampled = should_sample(request.span_id)
|
|
26
|
+
span = trace.get_current_span()
|
|
27
|
+
span.set_attribute("llm.is_sampled", sampled)
|
|
28
|
+
return SamplingGateResponse(is_sampled=sampled)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
import os
|
|
5
|
+
from opentelemetry import trace
|
|
6
|
+
from .....features.auto_instrumentation import (
|
|
7
|
+
init_auto_instrumentation,
|
|
8
|
+
uninstrument_all,
|
|
9
|
+
detect_llm_call,
|
|
10
|
+
trigger_test_call
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
router = APIRouter(prefix="/instrumentation", tags=["Instrumentation"])
|
|
15
|
+
|
|
16
|
+
class StatusResponse(BaseModel):
|
|
17
|
+
success: bool
|
|
18
|
+
message: str
|
|
19
|
+
|
|
20
|
+
class DetectionRequest(BaseModel):
|
|
21
|
+
url: str
|
|
22
|
+
body: str
|
|
23
|
+
|
|
24
|
+
class DetectionResponse(BaseModel):
|
|
25
|
+
provider: str
|
|
26
|
+
model: str
|
|
27
|
+
|
|
28
|
+
class TestCallRequest(BaseModel):
|
|
29
|
+
method: str
|
|
30
|
+
provider: str
|
|
31
|
+
|
|
32
|
+
def _set_span_attributes():
|
|
33
|
+
span = trace.get_current_span()
|
|
34
|
+
span.set_attribute("service.name", "instrumentation-sdk-api")
|
|
35
|
+
span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
|
|
36
|
+
span.set_attribute("feature.name", "auto_instrumentation")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@router.post("/init", response_model=StatusResponse)
|
|
40
|
+
def init_instrumentation():
|
|
41
|
+
_set_span_attributes()
|
|
42
|
+
try:
|
|
43
|
+
init_auto_instrumentation()
|
|
44
|
+
return StatusResponse(success=True, message="Auto-instrumentation initialized")
|
|
45
|
+
except Exception as e:
|
|
46
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
47
|
+
|
|
48
|
+
@router.post("/uninstrument", response_model=StatusResponse)
|
|
49
|
+
def uninstrument():
|
|
50
|
+
_set_span_attributes()
|
|
51
|
+
try:
|
|
52
|
+
uninstrument_all()
|
|
53
|
+
return StatusResponse(success=True, message="All instrumentation disabled")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
56
|
+
|
|
57
|
+
@router.post("/detect", response_model=DetectionResponse)
|
|
58
|
+
def detect(request: DetectionRequest):
|
|
59
|
+
_set_span_attributes()
|
|
60
|
+
result = detect_llm_call(request.url, request.body)
|
|
61
|
+
return DetectionResponse(**result)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.post("/test-call", response_model=StatusResponse)
|
|
67
|
+
async def test_call(request: TestCallRequest):
|
|
68
|
+
_set_span_attributes()
|
|
69
|
+
result = await trigger_test_call(request.method, request.provider)
|
|
70
|
+
if not result["success"]:
|
|
71
|
+
raise HTTPException(status_code=500, detail=result["message"])
|
|
72
|
+
return StatusResponse(**result)
|
|
73
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import Optional, Dict, Any, List
|
|
4
|
+
import os
|
|
5
|
+
from opentelemetry import trace
|
|
6
|
+
from .....features.metrics.index import record_span_metrics, init_metrics_pipeline
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
router = APIRouter(prefix="/metrics", tags=["Metrics"])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MetricsStatusResponse(BaseModel):
|
|
13
|
+
initialized: bool
|
|
14
|
+
message: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MetricsInitRequest(BaseModel):
|
|
18
|
+
port: Optional[int] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RecordSpanMetricsRequest(BaseModel):
|
|
22
|
+
model: str = "unknown"
|
|
23
|
+
provider: str = "unknown"
|
|
24
|
+
service_name: str = "unknown"
|
|
25
|
+
prompt_tokens: Optional[int] = None
|
|
26
|
+
completion_tokens: Optional[int] = None
|
|
27
|
+
cost_usd_micro: Optional[int] = None
|
|
28
|
+
latency_ms_total: Optional[int] = None
|
|
29
|
+
latency_ms_ttft: Optional[int] = None
|
|
30
|
+
finish_reason: Optional[str] = None
|
|
31
|
+
status: str = "success"
|
|
32
|
+
pii_detected: bool = False
|
|
33
|
+
injection_attempt: bool = False
|
|
34
|
+
retry_count: int = 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RecordSpanMetricsResponse(BaseModel):
|
|
38
|
+
recorded: bool
|
|
39
|
+
cost_usd_micro: Optional[int] = None
|
|
40
|
+
price_version: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BatchRecordRequest(BaseModel):
|
|
44
|
+
spans: List[RecordSpanMetricsRequest]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BatchRecordResponse(BaseModel):
|
|
48
|
+
recorded_count: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _set_span_attributes() -> None:
|
|
52
|
+
span = trace.get_current_span()
|
|
53
|
+
span.set_attribute("service.name", "instrumentation-sdk-api")
|
|
54
|
+
span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
|
|
55
|
+
span.set_attribute("feature.name", "metrics")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@router.post("/init", response_model=MetricsStatusResponse)
|
|
59
|
+
def init_metrics(request: MetricsInitRequest = MetricsInitRequest()):
|
|
60
|
+
_set_span_attributes()
|
|
61
|
+
try:
|
|
62
|
+
init_metrics_pipeline(request.port)
|
|
63
|
+
return MetricsStatusResponse(initialized=True, message="Metrics pipeline initialized")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.get("/health", response_model=MetricsStatusResponse)
|
|
69
|
+
def metrics_health():
|
|
70
|
+
_set_span_attributes()
|
|
71
|
+
from .....features.metrics.index import _initialized
|
|
72
|
+
return MetricsStatusResponse(
|
|
73
|
+
initialized=_initialized,
|
|
74
|
+
message="Metrics pipeline is active" if _initialized else "Metrics pipeline not initialized"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@router.post("/record", response_model=RecordSpanMetricsResponse)
|
|
79
|
+
def record_metrics(request: RecordSpanMetricsRequest):
|
|
80
|
+
_set_span_attributes()
|
|
81
|
+
try:
|
|
82
|
+
span_data = request.model_dump(exclude_none=True)
|
|
83
|
+
record_span_metrics(span_data)
|
|
84
|
+
return RecordSpanMetricsResponse(
|
|
85
|
+
recorded=True,
|
|
86
|
+
cost_usd_micro=span_data.get("cost_usd_micro"),
|
|
87
|
+
price_version=span_data.get("price_version"),
|
|
88
|
+
)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router.post("/record-batch", response_model=BatchRecordResponse)
|
|
94
|
+
def record_batch(request: BatchRecordRequest):
|
|
95
|
+
_set_span_attributes()
|
|
96
|
+
count = 0
|
|
97
|
+
for span_req in request.spans:
|
|
98
|
+
try:
|
|
99
|
+
span_data = span_req.model_dump(exclude_none=True)
|
|
100
|
+
record_span_metrics(span_data)
|
|
101
|
+
count += 1
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
return BatchRecordResponse(recorded_count=count)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
import os
|
|
4
|
+
from opentelemetry import trace
|
|
5
|
+
from .....features.minilm_embedding.index import get_embedding
|
|
6
|
+
|
|
7
|
+
router = APIRouter(prefix="/embeddings", tags=["MiniLM Embedding"])
|
|
8
|
+
|
|
9
|
+
class EmbeddingRequest(BaseModel):
|
|
10
|
+
text: str
|
|
11
|
+
|
|
12
|
+
class EmbeddingResponse(BaseModel):
|
|
13
|
+
embedding: list[float]
|
|
14
|
+
|
|
15
|
+
def _set_span_attributes() -> None:
|
|
16
|
+
span = trace.get_current_span()
|
|
17
|
+
span.set_attribute("service.name", "instrumentation-sdk-api")
|
|
18
|
+
span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
|
|
19
|
+
span.set_attribute("feature.name", "minilm_embedding")
|
|
20
|
+
|
|
21
|
+
@router.post("/embed", response_model=EmbeddingResponse)
|
|
22
|
+
async def embed_endpoint(request: EmbeddingRequest) -> EmbeddingResponse:
|
|
23
|
+
_set_span_attributes()
|
|
24
|
+
try:
|
|
25
|
+
emb = await get_embedding(request.text)
|
|
26
|
+
if emb is None:
|
|
27
|
+
raise HTTPException(status_code=500, detail="Failed to generate embedding")
|
|
28
|
+
return EmbeddingResponse(embedding=emb)
|
|
29
|
+
except HTTPException:
|
|
30
|
+
raise
|
|
31
|
+
except Exception as e:
|
|
32
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import Union, List, Dict, Any
|
|
4
|
+
import os
|
|
5
|
+
from opentelemetry import trace
|
|
6
|
+
from .....features.pii_injection_scan.index import scan_prompt
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/pii-injection", tags=["PII & Injection Scan"])
|
|
9
|
+
|
|
10
|
+
class PiiInjectionScanRequest(BaseModel):
|
|
11
|
+
prompt: Union[str, List[Dict[str, Any]]]
|
|
12
|
+
|
|
13
|
+
class PiiInjectionScanResponse(BaseModel):
|
|
14
|
+
pii_detected: bool
|
|
15
|
+
injection_attempt: bool
|
|
16
|
+
|
|
17
|
+
def _set_span_attributes() -> None:
|
|
18
|
+
span = trace.get_current_span()
|
|
19
|
+
span.set_attribute("service.name", "instrumentation-sdk-api")
|
|
20
|
+
span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
|
|
21
|
+
span.set_attribute("feature.name", "pii_injection_scan")
|
|
22
|
+
|
|
23
|
+
@router.post("/scan", response_model=PiiInjectionScanResponse)
|
|
24
|
+
def scan(request: PiiInjectionScanRequest) -> PiiInjectionScanResponse:
|
|
25
|
+
_set_span_attributes()
|
|
26
|
+
try:
|
|
27
|
+
pii, inj = scan_prompt(request.prompt)
|
|
28
|
+
span = trace.get_current_span()
|
|
29
|
+
span.set_attribute("llm.pii_detected", pii)
|
|
30
|
+
span.set_attribute("llm.injection_attempt", inj)
|
|
31
|
+
return PiiInjectionScanResponse(pii_detected=pii, injection_attempt=inj)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from fastapi.responses import StreamingResponse
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
import os
|
|
6
|
+
import asyncio
|
|
7
|
+
from opentelemetry import trace
|
|
8
|
+
from .....features.streaming.index import llm_streaming_span, wrap_async_stream
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/streaming", tags=["Streaming"])
|
|
11
|
+
|
|
12
|
+
class TestStreamRequest(BaseModel):
|
|
13
|
+
provider: str
|
|
14
|
+
chunks: Optional[List[str]] = None
|
|
15
|
+
|
|
16
|
+
def _set_span_attributes():
|
|
17
|
+
span = trace.get_current_span()
|
|
18
|
+
span.set_attribute("service.name", "instrumentation-sdk-api")
|
|
19
|
+
span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
|
|
20
|
+
span.set_attribute("feature.name", "streaming")
|
|
21
|
+
|
|
22
|
+
async def mock_async_stream(chunks: List[str]):
|
|
23
|
+
for chunk in chunks:
|
|
24
|
+
await asyncio.sleep(0.01)
|
|
25
|
+
yield chunk
|
|
26
|
+
|
|
27
|
+
@router.post("/test-stream-call")
|
|
28
|
+
async def test_stream_call(request: TestStreamRequest):
|
|
29
|
+
_set_span_attributes()
|
|
30
|
+
try:
|
|
31
|
+
chunks = request.chunks
|
|
32
|
+
if not chunks:
|
|
33
|
+
chunks = ["Hello ", "there! ", "This ", "is ", "a ", "mock ", "stream."]
|
|
34
|
+
|
|
35
|
+
span_ctx = llm_streaming_span(
|
|
36
|
+
span_type="llm_call",
|
|
37
|
+
provider=request.provider,
|
|
38
|
+
model="test-stream-model",
|
|
39
|
+
prompt="Triggered via API verification endpoint",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
wrapped_stream = wrap_async_stream(
|
|
43
|
+
mock_async_stream(chunks),
|
|
44
|
+
span_context=span_ctx,
|
|
45
|
+
model="test-stream-model"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def event_generator():
|
|
49
|
+
async with span_ctx:
|
|
50
|
+
async for chunk in wrapped_stream:
|
|
51
|
+
yield f"data: {chunk}\n\n"
|
|
52
|
+
|
|
53
|
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import Union, List, Dict, Any
|
|
4
|
+
import os
|
|
5
|
+
from opentelemetry import trace
|
|
6
|
+
from .....features.token_counting.index import count_tokens
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/token-counting", tags=["Token Counting"])
|
|
9
|
+
|
|
10
|
+
class TokenCountRequest(BaseModel):
|
|
11
|
+
prompt: Union[str, List[Dict[str, Any]]]
|
|
12
|
+
model: str
|
|
13
|
+
|
|
14
|
+
class TokenCountResponse(BaseModel):
|
|
15
|
+
tokens: int
|
|
16
|
+
method: str
|
|
17
|
+
|
|
18
|
+
def _set_span_attributes():
|
|
19
|
+
span = trace.get_current_span()
|
|
20
|
+
span.set_attribute("service.name", "instrumentation-sdk-api")
|
|
21
|
+
span.set_attribute("deployment.env", os.getenv("DEPLOYMENT_ENV", "dev"))
|
|
22
|
+
span.set_attribute("feature.name", "token_counting")
|
|
23
|
+
|
|
24
|
+
@router.post("/count", response_model=TokenCountResponse)
|
|
25
|
+
def count(request: TokenCountRequest):
|
|
26
|
+
_set_span_attributes()
|
|
27
|
+
try:
|
|
28
|
+
tokens, method = count_tokens(request.prompt, request.model)
|
|
29
|
+
return TokenCountResponse(tokens=tokens, method=method)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
from .handlers.instrumentation import router as instrumentation_router
|
|
3
|
+
from .handlers.token_counting import router as token_counting_router
|
|
4
|
+
from .handlers.streaming import router as streaming_router
|
|
5
|
+
from .handlers.pii_injection import router as pii_injection_router
|
|
6
|
+
from .handlers.metrics import router as metrics_router
|
|
7
|
+
from .handlers.deterministic_sampling import router as deterministic_sampling_router
|
|
8
|
+
from .handlers.minilm_embedding import router as minilm_embedding_router
|
|
9
|
+
|
|
10
|
+
api_v1_router = APIRouter()
|
|
11
|
+
api_v1_router.include_router(instrumentation_router)
|
|
12
|
+
api_v1_router.include_router(token_counting_router)
|
|
13
|
+
api_v1_router.include_router(streaming_router)
|
|
14
|
+
api_v1_router.include_router(pii_injection_router)
|
|
15
|
+
api_v1_router.include_router(metrics_router)
|
|
16
|
+
api_v1_router.include_router(deterministic_sampling_router)
|
|
17
|
+
api_v1_router.include_router(minilm_embedding_router)
|
|
18
|
+
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .index import (
|
|
2
|
+
init_auto_instrumentation,
|
|
3
|
+
uninstrument_all,
|
|
4
|
+
instrument_client,
|
|
5
|
+
instrument_http_client,
|
|
6
|
+
detect_llm_call,
|
|
7
|
+
trigger_test_call
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"init_auto_instrumentation",
|
|
12
|
+
"uninstrument_all",
|
|
13
|
+
"instrument_client",
|
|
14
|
+
"instrument_http_client",
|
|
15
|
+
"detect_llm_call",
|
|
16
|
+
"trigger_test_call"
|
|
17
|
+
]
|
|
18
|
+
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
from ...spans.types import FinishReason
|
|
3
|
+
|
|
4
|
+
class ProviderMapper:
|
|
5
|
+
@staticmethod
|
|
6
|
+
def map_openai_response(response: Any) -> Dict[str, Any]:
|
|
7
|
+
usage = getattr(response, "usage", None)
|
|
8
|
+
choices = getattr(response, "choices", [])
|
|
9
|
+
|
|
10
|
+
finish_reason = FinishReason.UNSPECIFIED
|
|
11
|
+
if choices:
|
|
12
|
+
reason = choices[0].finish_reason
|
|
13
|
+
if reason == "stop":
|
|
14
|
+
finish_reason = FinishReason.STOP
|
|
15
|
+
elif reason == "length":
|
|
16
|
+
finish_reason = FinishReason.LENGTH
|
|
17
|
+
elif reason == "content_filter":
|
|
18
|
+
finish_reason = FinishReason.CONTENT_FILTER
|
|
19
|
+
elif reason == "tool_calls":
|
|
20
|
+
finish_reason = FinishReason.TOOL_CALLS
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
"model": response.model,
|
|
24
|
+
"provider": "openai",
|
|
25
|
+
"prompt_tokens": usage.prompt_tokens if usage else 1,
|
|
26
|
+
"completion_tokens": usage.completion_tokens if usage else 0,
|
|
27
|
+
"finish_reason": finish_reason,
|
|
28
|
+
"response_content": choices[0].message.content if choices and hasattr(choices[0].message, "content") else None
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def map_anthropic_response(response: Any) -> Dict[str, Any]:
|
|
33
|
+
usage = getattr(response, "usage", None)
|
|
34
|
+
|
|
35
|
+
finish_reason = FinishReason.UNSPECIFIED
|
|
36
|
+
stop_reason = getattr(response, "stop_reason", None)
|
|
37
|
+
if stop_reason == "end_turn":
|
|
38
|
+
finish_reason = FinishReason.STOP
|
|
39
|
+
elif stop_reason == "max_tokens":
|
|
40
|
+
finish_reason = FinishReason.LENGTH
|
|
41
|
+
elif stop_reason == "stop_sequence":
|
|
42
|
+
finish_reason = FinishReason.STOP
|
|
43
|
+
elif stop_reason == "tool_use":
|
|
44
|
+
finish_reason = FinishReason.TOOL_CALLS
|
|
45
|
+
|
|
46
|
+
content = ""
|
|
47
|
+
if hasattr(response, "content") and response.content:
|
|
48
|
+
content = response.content[0].text if hasattr(response.content[0], "text") else ""
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"model": response.model,
|
|
52
|
+
"provider": "anthropic",
|
|
53
|
+
"prompt_tokens": usage.input_tokens if usage else 1,
|
|
54
|
+
"completion_tokens": usage.output_tokens if usage else 0,
|
|
55
|
+
"finish_reason": finish_reason,
|
|
56
|
+
"response_content": content
|
|
57
|
+
}
|
|
58
|
+
@staticmethod
|
|
59
|
+
def map_langchain_response(response: Any, model: str, provider: str) -> Dict[str, Any]:
|
|
60
|
+
usage = getattr(response, "usage_metadata", {})
|
|
61
|
+
if not usage:
|
|
62
|
+
usage = {}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"model": model,
|
|
66
|
+
"provider": f"langchain:{provider}",
|
|
67
|
+
"prompt_tokens": usage.get("input_tokens", 1),
|
|
68
|
+
"completion_tokens": usage.get("output_tokens", 0),
|
|
69
|
+
"finish_reason": FinishReason.STOP if hasattr(response, "content") else FinishReason.UNSPECIFIED,
|
|
70
|
+
"response_content": getattr(response, "content", "")
|
|
71
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from .service import AutoInstrumentationService
|
|
2
|
+
from .infra.patchers.openai_patcher import OpenAIPatcher
|
|
3
|
+
from .infra.patchers.anthropic_patcher import AnthropicPatcher
|
|
4
|
+
from .infra.patchers.litellm_patcher import LiteLLMPatcher
|
|
5
|
+
from .infra.patchers.langchain_patcher import LangChainPatcher
|
|
6
|
+
from .infra.patchers.http_patcher import HTTPPatcher
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
|
|
10
|
+
_SERVICE = AutoInstrumentationService(
|
|
11
|
+
patchers=[
|
|
12
|
+
OpenAIPatcher(),
|
|
13
|
+
AnthropicPatcher(),
|
|
14
|
+
LiteLLMPatcher(),
|
|
15
|
+
LangChainPatcher(),
|
|
16
|
+
HTTPPatcher()
|
|
17
|
+
]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def init_auto_instrumentation() -> None:
|
|
21
|
+
_SERVICE.instrument_all()
|
|
22
|
+
|
|
23
|
+
def uninstrument_all() -> None:
|
|
24
|
+
_SERVICE.uninstrument_all()
|
|
25
|
+
|
|
26
|
+
def instrument_client(client: Any, provider: str) -> None:
|
|
27
|
+
_SERVICE.instrument_client(client, provider)
|
|
28
|
+
|
|
29
|
+
def instrument_http_client(client: Any, provider: Optional[str] = None) -> None:
|
|
30
|
+
for patcher in _SERVICE._patchers:
|
|
31
|
+
if isinstance(patcher, HTTPPatcher):
|
|
32
|
+
patcher.patch_instance(client)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
def detect_llm_call(url: str, body: str) -> Dict[str, str]:
|
|
36
|
+
for patcher in _SERVICE._patchers:
|
|
37
|
+
if isinstance(patcher, HTTPPatcher):
|
|
38
|
+
provider = patcher._detect_provider(url)
|
|
39
|
+
model = patcher._extract_model(body)
|
|
40
|
+
return {"provider": provider or "unknown", "model": model}
|
|
41
|
+
return {"provider": "unknown", "model": "unknown"}
|
|
42
|
+
|
|
43
|
+
async def trigger_test_call(method: str, provider: str) -> Dict[str, Any]:
|
|
44
|
+
import httpx
|
|
45
|
+
import json
|
|
46
|
+
|
|
47
|
+
url_map = {
|
|
48
|
+
"openai": "https://api.openai.com/v1/chat/completions",
|
|
49
|
+
"anthropic": "https://api.anthropic.com/v1/messages"
|
|
50
|
+
}
|
|
51
|
+
url = url_map.get(provider, "https://api.openai.com/v1/chat/completions")
|
|
52
|
+
payload = {"model": "instrumentation-test-model"}
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
if method == "httpx":
|
|
56
|
+
async with httpx.AsyncClient() as client:
|
|
57
|
+
try:
|
|
58
|
+
await client.post(url, json=payload, timeout=1.0)
|
|
59
|
+
except httpx.HTTPError:
|
|
60
|
+
pass
|
|
61
|
+
elif method == "requests":
|
|
62
|
+
import requests
|
|
63
|
+
try:
|
|
64
|
+
requests.post(url, json=payload, timeout=1.0)
|
|
65
|
+
except requests.RequestException:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
return {"success": True, "message": f"Test call triggered via {method} for {provider}"}
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return {"success": False, "message": str(e)}
|
|
File without changes
|
|
File without changes
|