pisama-auto 0.1.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.
- pisama_auto-0.1.0/.github/workflows/ci.yml +32 -0
- pisama_auto-0.1.0/.gitignore +114 -0
- pisama_auto-0.1.0/LICENSE +21 -0
- pisama_auto-0.1.0/PKG-INFO +85 -0
- pisama_auto-0.1.0/README.md +61 -0
- pisama_auto-0.1.0/pyproject.toml +40 -0
- pisama_auto-0.1.0/src/pisama_auto/__init__.py +79 -0
- pisama_auto-0.1.0/src/pisama_auto/_tracer.py +59 -0
- pisama_auto-0.1.0/src/pisama_auto/patches/__init__.py +70 -0
- pisama_auto-0.1.0/src/pisama_auto/patches/anthropic_patch.py +154 -0
- pisama_auto-0.1.0/src/pisama_auto/patches/openai_patch.py +83 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: |
|
|
26
|
+
pip install -e ".[dev]"
|
|
27
|
+
|
|
28
|
+
- name: Lint
|
|
29
|
+
run: ruff check src/
|
|
30
|
+
|
|
31
|
+
- name: Test
|
|
32
|
+
run: pytest tests/ -q --tb=short
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
/lib/
|
|
14
|
+
/lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
venv/
|
|
25
|
+
.venv/
|
|
26
|
+
ENV/
|
|
27
|
+
env/
|
|
28
|
+
|
|
29
|
+
# IDE
|
|
30
|
+
.idea/
|
|
31
|
+
.vscode/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
*~
|
|
35
|
+
|
|
36
|
+
# Testing
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
.coverage
|
|
39
|
+
htmlcov/
|
|
40
|
+
.tox/
|
|
41
|
+
.nox/
|
|
42
|
+
backend/backend/
|
|
43
|
+
|
|
44
|
+
# Generated test results and runtime data
|
|
45
|
+
detector_test_results_*.json
|
|
46
|
+
backend/data/fix_feedback.jsonl
|
|
47
|
+
backend/data/progress_log.jsonl
|
|
48
|
+
backend/data/mast_test_results*.json
|
|
49
|
+
backend/data/otel_test_results.json
|
|
50
|
+
backend/data/calibration_history.jsonl
|
|
51
|
+
backend/data/calibration_report.json
|
|
52
|
+
backend/data/baselines/
|
|
53
|
+
backend/app/data/
|
|
54
|
+
|
|
55
|
+
# Playwright
|
|
56
|
+
frontend/tests/auth/storage-state.json
|
|
57
|
+
frontend/test-results/
|
|
58
|
+
frontend/playwright-report/
|
|
59
|
+
frontend/playwright/.cache/
|
|
60
|
+
|
|
61
|
+
# Environment
|
|
62
|
+
.env
|
|
63
|
+
.env.*
|
|
64
|
+
!.env.example
|
|
65
|
+
!.env.test
|
|
66
|
+
|
|
67
|
+
# Vercel
|
|
68
|
+
.vercel
|
|
69
|
+
|
|
70
|
+
# Scraped traces (large data files) - only at root level
|
|
71
|
+
/traces/
|
|
72
|
+
|
|
73
|
+
# macOS
|
|
74
|
+
.DS_Store
|
|
75
|
+
|
|
76
|
+
# Secrets - never commit
|
|
77
|
+
deployment-env-vars.md
|
|
78
|
+
frontend/.env.local
|
|
79
|
+
|
|
80
|
+
# Secrets and credentials
|
|
81
|
+
*.pem
|
|
82
|
+
*.key
|
|
83
|
+
*.crt
|
|
84
|
+
credentials.json
|
|
85
|
+
*.secret
|
|
86
|
+
|
|
87
|
+
# Node (frontend)
|
|
88
|
+
node_modules/
|
|
89
|
+
.next/
|
|
90
|
+
out/
|
|
91
|
+
|
|
92
|
+
# Logs
|
|
93
|
+
*.log
|
|
94
|
+
logs/
|
|
95
|
+
|
|
96
|
+
# Local config
|
|
97
|
+
*.local
|
|
98
|
+
models/
|
|
99
|
+
_archived/
|
|
100
|
+
|
|
101
|
+
# Sensitive environment files (never commit)
|
|
102
|
+
frontend/.env.local
|
|
103
|
+
frontend/.env.vercel.production
|
|
104
|
+
.env.local
|
|
105
|
+
|
|
106
|
+
# Confidential docs (strategy, pricing, competitive intel)
|
|
107
|
+
docs-internal/
|
|
108
|
+
|
|
109
|
+
# Terraform
|
|
110
|
+
.terraform/
|
|
111
|
+
*.tfstate
|
|
112
|
+
*.tfstate.*
|
|
113
|
+
.terraform.lock.hcl
|
|
114
|
+
backend/models/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Pisama (tn-pisama)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pisama-auto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-code auto-instrumentation for LLM applications — adds Pisama failure detection with one line
|
|
5
|
+
Project-URL: Homepage, https://github.com/tn-pisama/pisama-auto
|
|
6
|
+
Project-URL: Documentation, https://docs.pisama.ai/auto-instrumentation
|
|
7
|
+
Author-email: Pisama Team <team@pisama.ai>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,ai,auto-instrumentation,llm,monitoring,observability,opentelemetry
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
20
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0
|
|
21
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
22
|
+
Requires-Dist: wrapt>=1.15.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# pisama-auto
|
|
26
|
+
|
|
27
|
+
Zero-code auto-instrumentation for LLM applications. Add Pisama failure detection with one line.
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pisama-auto
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import pisama_auto
|
|
37
|
+
pisama_auto.init() # traces locally; set PISAMA_API_KEY + PISAMA_ENDPOINT to export
|
|
38
|
+
|
|
39
|
+
# All subsequent LLM calls are automatically traced
|
|
40
|
+
import anthropic
|
|
41
|
+
client = anthropic.Anthropic()
|
|
42
|
+
response = client.messages.create(
|
|
43
|
+
model="claude-sonnet-4-6",
|
|
44
|
+
max_tokens=1024,
|
|
45
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
46
|
+
)
|
|
47
|
+
# ^ This call is automatically traced (and sent to Pisama if endpoint is configured)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Supported Libraries
|
|
51
|
+
|
|
52
|
+
| Library | Status | What's Traced |
|
|
53
|
+
|---------|--------|---------------|
|
|
54
|
+
| `anthropic` | GA | `messages.create()`, `messages.stream()` |
|
|
55
|
+
| `openai` | GA | `chat.completions.create()` |
|
|
56
|
+
|
|
57
|
+
## How It Works
|
|
58
|
+
|
|
59
|
+
1. `pisama_auto.init()` sets up an OpenTelemetry tracer that exports to Pisama
|
|
60
|
+
2. It then patches supported LLM libraries to emit spans with `gen_ai.*` semantic conventions
|
|
61
|
+
3. Pisama's detection engine analyzes the traces for 44 failure modes
|
|
62
|
+
4. Results appear in your Pisama dashboard
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
pisama_auto.init(
|
|
68
|
+
api_key="ps_...", # or set PISAMA_API_KEY env var
|
|
69
|
+
endpoint="https://your-instance/api/v1/traces/ingest", # or PISAMA_ENDPOINT env var
|
|
70
|
+
service_name="my-agent", # OTEL service name
|
|
71
|
+
auto_patch=True, # auto-patch all detected libraries
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Without an endpoint, traces are generated locally but not exported. Set `PISAMA_ENDPOINT` to send them to the Pisama platform.
|
|
76
|
+
|
|
77
|
+
## Selective Patching
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import pisama_auto
|
|
81
|
+
pisama_auto.init(auto_patch=False) # don't auto-patch
|
|
82
|
+
|
|
83
|
+
from pisama_auto.patches import patch
|
|
84
|
+
patch("anthropic") # only patch anthropic
|
|
85
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# pisama-auto
|
|
2
|
+
|
|
3
|
+
Zero-code auto-instrumentation for LLM applications. Add Pisama failure detection with one line.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pisama-auto
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import pisama_auto
|
|
13
|
+
pisama_auto.init() # traces locally; set PISAMA_API_KEY + PISAMA_ENDPOINT to export
|
|
14
|
+
|
|
15
|
+
# All subsequent LLM calls are automatically traced
|
|
16
|
+
import anthropic
|
|
17
|
+
client = anthropic.Anthropic()
|
|
18
|
+
response = client.messages.create(
|
|
19
|
+
model="claude-sonnet-4-6",
|
|
20
|
+
max_tokens=1024,
|
|
21
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
22
|
+
)
|
|
23
|
+
# ^ This call is automatically traced (and sent to Pisama if endpoint is configured)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Supported Libraries
|
|
27
|
+
|
|
28
|
+
| Library | Status | What's Traced |
|
|
29
|
+
|---------|--------|---------------|
|
|
30
|
+
| `anthropic` | GA | `messages.create()`, `messages.stream()` |
|
|
31
|
+
| `openai` | GA | `chat.completions.create()` |
|
|
32
|
+
|
|
33
|
+
## How It Works
|
|
34
|
+
|
|
35
|
+
1. `pisama_auto.init()` sets up an OpenTelemetry tracer that exports to Pisama
|
|
36
|
+
2. It then patches supported LLM libraries to emit spans with `gen_ai.*` semantic conventions
|
|
37
|
+
3. Pisama's detection engine analyzes the traces for 44 failure modes
|
|
38
|
+
4. Results appear in your Pisama dashboard
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
pisama_auto.init(
|
|
44
|
+
api_key="ps_...", # or set PISAMA_API_KEY env var
|
|
45
|
+
endpoint="https://your-instance/api/v1/traces/ingest", # or PISAMA_ENDPOINT env var
|
|
46
|
+
service_name="my-agent", # OTEL service name
|
|
47
|
+
auto_patch=True, # auto-patch all detected libraries
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Without an endpoint, traces are generated locally but not exported. Set `PISAMA_ENDPOINT` to send them to the Pisama platform.
|
|
52
|
+
|
|
53
|
+
## Selective Patching
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import pisama_auto
|
|
57
|
+
pisama_auto.init(auto_patch=False) # don't auto-patch
|
|
58
|
+
|
|
59
|
+
from pisama_auto.patches import patch
|
|
60
|
+
patch("anthropic") # only patch anthropic
|
|
61
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pisama-auto"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Zero-code auto-instrumentation for LLM applications — adds Pisama failure detection with one line"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Pisama Team", email = "team@pisama.ai" }
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"ai", "agents", "monitoring", "auto-instrumentation",
|
|
17
|
+
"opentelemetry", "observability", "llm"
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"opentelemetry-api>=1.20.0",
|
|
30
|
+
"opentelemetry-sdk>=1.20.0",
|
|
31
|
+
"opentelemetry-exporter-otlp-proto-http>=1.20.0",
|
|
32
|
+
"wrapt>=1.15.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/tn-pisama/pisama-auto"
|
|
37
|
+
Documentation = "https://docs.pisama.ai/auto-instrumentation"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/pisama_auto"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Pisama Auto-Instrumentation.
|
|
2
|
+
|
|
3
|
+
Zero-code instrumentation for LLM applications.
|
|
4
|
+
Automatically patches supported libraries to emit OTEL traces
|
|
5
|
+
that Pisama can analyze for failure detection.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
import pisama_auto
|
|
9
|
+
pisama_auto.init(api_key="ps_...")
|
|
10
|
+
|
|
11
|
+
# All subsequent LLM calls are automatically traced
|
|
12
|
+
import anthropic
|
|
13
|
+
client = anthropic.Anthropic()
|
|
14
|
+
response = client.messages.create(...) # <-- automatically traced
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("pisama_auto")
|
|
21
|
+
|
|
22
|
+
_initialized = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def init(
|
|
26
|
+
api_key: Optional[str] = None,
|
|
27
|
+
endpoint: Optional[str] = None,
|
|
28
|
+
service_name: str = "pisama-auto",
|
|
29
|
+
auto_patch: bool = True,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize Pisama auto-instrumentation.
|
|
32
|
+
|
|
33
|
+
Sets up OTEL tracing and patches supported LLM libraries to automatically
|
|
34
|
+
emit traces that Pisama can analyze.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
api_key: Pisama API key (ps_...). Also reads PISAMA_API_KEY env var.
|
|
38
|
+
endpoint: Pisama OTEL ingestion endpoint. Also reads PISAMA_ENDPOINT env var.
|
|
39
|
+
If not set, traces are generated locally but not exported.
|
|
40
|
+
service_name: Service name for OTEL resource.
|
|
41
|
+
auto_patch: If True, automatically patch all detected libraries.
|
|
42
|
+
"""
|
|
43
|
+
global _initialized
|
|
44
|
+
if _initialized:
|
|
45
|
+
logger.debug("Pisama auto-instrumentation already initialized")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
import os
|
|
49
|
+
api_key = api_key or os.environ.get("PISAMA_API_KEY")
|
|
50
|
+
endpoint = endpoint or os.environ.get("PISAMA_ENDPOINT")
|
|
51
|
+
|
|
52
|
+
if not api_key:
|
|
53
|
+
logger.warning(
|
|
54
|
+
"No Pisama API key provided. Set PISAMA_API_KEY or pass api_key to init(). "
|
|
55
|
+
"Traces will be generated but not exported."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Set up OTEL tracer
|
|
59
|
+
from ._tracer import setup_tracer
|
|
60
|
+
tracer_kwargs = {"api_key": api_key, "service_name": service_name}
|
|
61
|
+
if endpoint:
|
|
62
|
+
tracer_kwargs["endpoint"] = endpoint
|
|
63
|
+
setup_tracer(**tracer_kwargs)
|
|
64
|
+
|
|
65
|
+
# Auto-patch detected libraries
|
|
66
|
+
if auto_patch:
|
|
67
|
+
from .patches import patch_all
|
|
68
|
+
patched = patch_all()
|
|
69
|
+
if patched:
|
|
70
|
+
logger.info(f"Pisama: auto-instrumented {', '.join(patched)}")
|
|
71
|
+
else:
|
|
72
|
+
logger.info("Pisama: initialized (no patchable libraries detected yet)")
|
|
73
|
+
|
|
74
|
+
_initialized = True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_initialized() -> bool:
|
|
78
|
+
"""Check if Pisama auto-instrumentation is initialized."""
|
|
79
|
+
return _initialized
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""OTEL tracer setup for Pisama auto-instrumentation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from opentelemetry import trace
|
|
7
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
8
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
9
|
+
from opentelemetry.sdk.resources import Resource
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("pisama_auto")
|
|
12
|
+
|
|
13
|
+
_tracer: Optional[trace.Tracer] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def setup_tracer(
|
|
17
|
+
api_key: Optional[str] = None,
|
|
18
|
+
endpoint: str = "https://pisama-api.fly.dev/api/v1/traces/ingest",
|
|
19
|
+
service_name: str = "pisama-auto",
|
|
20
|
+
) -> trace.Tracer:
|
|
21
|
+
"""Set up the OTEL tracer with Pisama exporter."""
|
|
22
|
+
global _tracer
|
|
23
|
+
|
|
24
|
+
resource = Resource.create({
|
|
25
|
+
"service.name": service_name,
|
|
26
|
+
"pisama.sdk": "pisama-auto",
|
|
27
|
+
"pisama.sdk.version": "0.1.0",
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
provider = TracerProvider(resource=resource)
|
|
31
|
+
|
|
32
|
+
if api_key:
|
|
33
|
+
try:
|
|
34
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
35
|
+
|
|
36
|
+
exporter = OTLPSpanExporter(
|
|
37
|
+
endpoint=endpoint,
|
|
38
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
39
|
+
)
|
|
40
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
41
|
+
logger.debug(f"Pisama OTEL exporter configured: {endpoint}")
|
|
42
|
+
except ImportError:
|
|
43
|
+
logger.warning("OTLP exporter not available, using console exporter")
|
|
44
|
+
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
|
45
|
+
else:
|
|
46
|
+
# No API key — still trace locally for debugging
|
|
47
|
+
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
|
48
|
+
|
|
49
|
+
trace.set_tracer_provider(provider)
|
|
50
|
+
_tracer = trace.get_tracer("pisama_auto", "0.1.0")
|
|
51
|
+
return _tracer
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_tracer() -> trace.Tracer:
|
|
55
|
+
"""Get the Pisama tracer. Sets up a default if not initialized."""
|
|
56
|
+
global _tracer
|
|
57
|
+
if _tracer is None:
|
|
58
|
+
_tracer = trace.get_tracer("pisama_auto", "0.1.0")
|
|
59
|
+
return _tracer
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Auto-patching for LLM libraries.
|
|
2
|
+
|
|
3
|
+
Detects installed libraries and patches them to emit OTEL spans
|
|
4
|
+
with gen_ai.* semantic conventions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
import logging
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("pisama_auto")
|
|
12
|
+
|
|
13
|
+
# Map of library name -> patch module
|
|
14
|
+
_PATCHABLE = {
|
|
15
|
+
"anthropic": ".anthropic_patch",
|
|
16
|
+
"openai": ".openai_patch",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_patched: List[str] = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def patch_all() -> List[str]:
|
|
23
|
+
"""Patch all detected LLM libraries.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List of library names that were successfully patched.
|
|
27
|
+
"""
|
|
28
|
+
for lib_name, patch_module in _PATCHABLE.items():
|
|
29
|
+
if lib_name in _patched:
|
|
30
|
+
continue
|
|
31
|
+
try:
|
|
32
|
+
importlib.import_module(lib_name)
|
|
33
|
+
except ImportError:
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
mod = importlib.import_module(patch_module, package="pisama_auto.patches")
|
|
38
|
+
mod.patch()
|
|
39
|
+
_patched.append(lib_name)
|
|
40
|
+
logger.debug(f"Patched {lib_name}")
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.warning(f"Failed to patch {lib_name}: {e}")
|
|
43
|
+
|
|
44
|
+
return list(_patched)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def patch(library: str) -> bool:
|
|
48
|
+
"""Patch a specific library.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
library: Library name (anthropic, openai)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
True if patched successfully
|
|
55
|
+
"""
|
|
56
|
+
if library in _patched:
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
if library not in _PATCHABLE:
|
|
60
|
+
logger.warning(f"Unknown library: {library}. Supported: {list(_PATCHABLE.keys())}")
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
mod = importlib.import_module(_PATCHABLE[library], package="pisama_auto.patches")
|
|
65
|
+
mod.patch()
|
|
66
|
+
_patched.append(library)
|
|
67
|
+
return True
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.warning(f"Failed to patch {library}: {e}")
|
|
70
|
+
return False
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Auto-instrumentation patch for the Anthropic Python SDK.
|
|
2
|
+
|
|
3
|
+
Wraps anthropic.Anthropic.messages.create() and .stream() to emit
|
|
4
|
+
OTEL spans with gen_ai.* semantic conventions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import wrapt
|
|
11
|
+
from opentelemetry.trace import StatusCode
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("pisama_auto")
|
|
14
|
+
|
|
15
|
+
_original_create = None
|
|
16
|
+
_original_stream = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def patch() -> None:
|
|
20
|
+
"""Patch the Anthropic SDK to emit OTEL spans."""
|
|
21
|
+
import anthropic
|
|
22
|
+
|
|
23
|
+
messages_cls = anthropic.resources.Messages
|
|
24
|
+
|
|
25
|
+
global _original_create, _original_stream
|
|
26
|
+
_original_create = messages_cls.create
|
|
27
|
+
_original_stream = getattr(messages_cls, "stream", None)
|
|
28
|
+
|
|
29
|
+
wrapt.wrap_function_wrapper(messages_cls, "create", _traced_create)
|
|
30
|
+
|
|
31
|
+
if _original_stream:
|
|
32
|
+
wrapt.wrap_function_wrapper(messages_cls, "stream", _traced_stream)
|
|
33
|
+
|
|
34
|
+
logger.debug("Anthropic SDK patched")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _traced_create(wrapped, instance, args, kwargs) -> Any:
|
|
38
|
+
"""Wrapper for messages.create that adds OTEL tracing."""
|
|
39
|
+
from pisama_auto._tracer import get_tracer
|
|
40
|
+
|
|
41
|
+
tracer = get_tracer()
|
|
42
|
+
model = kwargs.get("model", "unknown")
|
|
43
|
+
span_name = f"gen_ai.chat {model}"
|
|
44
|
+
|
|
45
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
46
|
+
# Set gen_ai.* attributes
|
|
47
|
+
span.set_attribute("gen_ai.system", "anthropic")
|
|
48
|
+
span.set_attribute("gen_ai.request.model", model)
|
|
49
|
+
span.set_attribute("gen_ai.operation.name", "chat")
|
|
50
|
+
|
|
51
|
+
max_tokens = kwargs.get("max_tokens")
|
|
52
|
+
if max_tokens:
|
|
53
|
+
span.set_attribute("gen_ai.request.max_tokens", max_tokens)
|
|
54
|
+
|
|
55
|
+
temperature = kwargs.get("temperature")
|
|
56
|
+
if temperature is not None:
|
|
57
|
+
span.set_attribute("gen_ai.request.temperature", temperature)
|
|
58
|
+
|
|
59
|
+
# Record input messages count
|
|
60
|
+
messages = kwargs.get("messages", [])
|
|
61
|
+
span.set_attribute("gen_ai.request.messages_count", len(messages))
|
|
62
|
+
|
|
63
|
+
system = kwargs.get("system")
|
|
64
|
+
if system:
|
|
65
|
+
span.set_attribute("gen_ai.request.has_system", True)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
response = wrapped(*args, **kwargs)
|
|
69
|
+
|
|
70
|
+
# Record response attributes
|
|
71
|
+
span.set_attribute("gen_ai.response.model", getattr(response, "model", model))
|
|
72
|
+
span.set_attribute("gen_ai.response.stop_reason", getattr(response, "stop_reason", ""))
|
|
73
|
+
|
|
74
|
+
usage = getattr(response, "usage", None)
|
|
75
|
+
if usage:
|
|
76
|
+
input_tokens = getattr(usage, "input_tokens", 0)
|
|
77
|
+
output_tokens = getattr(usage, "output_tokens", 0)
|
|
78
|
+
span.set_attribute("gen_ai.usage.input_tokens", input_tokens)
|
|
79
|
+
span.set_attribute("gen_ai.usage.output_tokens", output_tokens)
|
|
80
|
+
span.set_attribute("gen_ai.usage.total_tokens", input_tokens + output_tokens)
|
|
81
|
+
|
|
82
|
+
return response
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
span.set_attribute("error.type", type(e).__name__)
|
|
86
|
+
span.set_attribute("error.message", str(e)[:500])
|
|
87
|
+
span.set_status(StatusCode.ERROR, str(e)[:200])
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _TracedStream:
|
|
92
|
+
"""Wrapper that ends the OTEL span when the stream is consumed or closed."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, stream, span):
|
|
95
|
+
self._stream = stream
|
|
96
|
+
self._span = span
|
|
97
|
+
|
|
98
|
+
def __enter__(self):
|
|
99
|
+
self._stream.__enter__()
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
103
|
+
try:
|
|
104
|
+
result = self._stream.__exit__(exc_type, exc_val, exc_tb)
|
|
105
|
+
if exc_type:
|
|
106
|
+
self._span.set_attribute("error.type", exc_type.__name__)
|
|
107
|
+
self._span.set_status(StatusCode.ERROR, str(exc_val)[:200])
|
|
108
|
+
return result
|
|
109
|
+
finally:
|
|
110
|
+
self._span.end()
|
|
111
|
+
|
|
112
|
+
def __iter__(self):
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def __next__(self):
|
|
116
|
+
try:
|
|
117
|
+
return next(self._stream)
|
|
118
|
+
except StopIteration:
|
|
119
|
+
self._span.end()
|
|
120
|
+
raise
|
|
121
|
+
except Exception as e:
|
|
122
|
+
self._span.set_attribute("error.type", type(e).__name__)
|
|
123
|
+
self._span.set_status(StatusCode.ERROR, str(e)[:200])
|
|
124
|
+
self._span.end()
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
def __getattr__(self, name):
|
|
128
|
+
return getattr(self._stream, name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _traced_stream(wrapped, instance, args, kwargs) -> Any:
|
|
132
|
+
"""Wrapper for messages.stream that adds OTEL tracing."""
|
|
133
|
+
from pisama_auto._tracer import get_tracer
|
|
134
|
+
|
|
135
|
+
tracer = get_tracer()
|
|
136
|
+
model = kwargs.get("model", "unknown")
|
|
137
|
+
span_name = f"gen_ai.chat.stream {model}"
|
|
138
|
+
|
|
139
|
+
span = tracer.start_span(span_name)
|
|
140
|
+
span.set_attribute("gen_ai.system", "anthropic")
|
|
141
|
+
span.set_attribute("gen_ai.request.model", model)
|
|
142
|
+
span.set_attribute("gen_ai.operation.name", "chat.stream")
|
|
143
|
+
|
|
144
|
+
messages = kwargs.get("messages", [])
|
|
145
|
+
span.set_attribute("gen_ai.request.messages_count", len(messages))
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
result = wrapped(*args, **kwargs)
|
|
149
|
+
return _TracedStream(result, span)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
span.set_attribute("error.type", type(e).__name__)
|
|
152
|
+
span.set_status(StatusCode.ERROR, str(e)[:200])
|
|
153
|
+
span.end()
|
|
154
|
+
raise
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Auto-instrumentation patch for the OpenAI Python SDK.
|
|
2
|
+
|
|
3
|
+
Wraps openai.ChatCompletion.create() and the v1+ client to emit
|
|
4
|
+
OTEL spans with gen_ai.* semantic conventions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import wrapt
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("pisama_auto")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def patch() -> None:
|
|
16
|
+
"""Patch the OpenAI SDK to emit OTEL spans."""
|
|
17
|
+
import openai
|
|
18
|
+
|
|
19
|
+
# OpenAI v1+ uses client.chat.completions.create()
|
|
20
|
+
if hasattr(openai, "resources"):
|
|
21
|
+
completions_cls = openai.resources.chat.Completions
|
|
22
|
+
wrapt.wrap_function_wrapper(completions_cls, "create", _traced_create)
|
|
23
|
+
logger.debug("OpenAI SDK v1+ patched")
|
|
24
|
+
else:
|
|
25
|
+
logger.warning("OpenAI SDK version not supported for auto-patching")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _traced_create(wrapped, instance, args, kwargs) -> Any:
|
|
29
|
+
"""Wrapper for chat.completions.create that adds OTEL tracing."""
|
|
30
|
+
from pisama_auto._tracer import get_tracer
|
|
31
|
+
|
|
32
|
+
tracer = get_tracer()
|
|
33
|
+
model = kwargs.get("model", "unknown")
|
|
34
|
+
span_name = f"gen_ai.chat {model}"
|
|
35
|
+
|
|
36
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
37
|
+
span.set_attribute("gen_ai.system", "openai")
|
|
38
|
+
span.set_attribute("gen_ai.request.model", model)
|
|
39
|
+
span.set_attribute("gen_ai.operation.name", "chat")
|
|
40
|
+
|
|
41
|
+
max_tokens = kwargs.get("max_tokens")
|
|
42
|
+
if max_tokens:
|
|
43
|
+
span.set_attribute("gen_ai.request.max_tokens", max_tokens)
|
|
44
|
+
|
|
45
|
+
temperature = kwargs.get("temperature")
|
|
46
|
+
if temperature is not None:
|
|
47
|
+
span.set_attribute("gen_ai.request.temperature", temperature)
|
|
48
|
+
|
|
49
|
+
messages = kwargs.get("messages", [])
|
|
50
|
+
span.set_attribute("gen_ai.request.messages_count", len(messages))
|
|
51
|
+
|
|
52
|
+
stream = kwargs.get("stream", False)
|
|
53
|
+
if stream:
|
|
54
|
+
span.set_attribute("gen_ai.operation.name", "chat.stream")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
response = wrapped(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
if not stream and response:
|
|
60
|
+
resp_model = getattr(response, "model", model)
|
|
61
|
+
span.set_attribute("gen_ai.response.model", resp_model)
|
|
62
|
+
|
|
63
|
+
choices = getattr(response, "choices", [])
|
|
64
|
+
if choices:
|
|
65
|
+
finish_reason = getattr(choices[0], "finish_reason", "")
|
|
66
|
+
span.set_attribute("gen_ai.response.finish_reason", finish_reason or "")
|
|
67
|
+
|
|
68
|
+
usage = getattr(response, "usage", None)
|
|
69
|
+
if usage:
|
|
70
|
+
input_tokens = getattr(usage, "prompt_tokens", 0)
|
|
71
|
+
output_tokens = getattr(usage, "completion_tokens", 0)
|
|
72
|
+
span.set_attribute("gen_ai.usage.input_tokens", input_tokens)
|
|
73
|
+
span.set_attribute("gen_ai.usage.output_tokens", output_tokens)
|
|
74
|
+
span.set_attribute("gen_ai.usage.total_tokens", input_tokens + output_tokens)
|
|
75
|
+
|
|
76
|
+
return response
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
span.set_attribute("error.type", type(e).__name__)
|
|
80
|
+
span.set_attribute("error.message", str(e)[:500])
|
|
81
|
+
from opentelemetry.trace import StatusCode
|
|
82
|
+
span.set_status(StatusCode.ERROR, str(e)[:200])
|
|
83
|
+
raise
|