pisama-claude-code 0.3.7__tar.gz → 0.4.2__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_claude_code-0.3.7 → pisama_claude_code-0.4.2}/CHANGELOG.md +14 -1
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/PKG-INFO +50 -3
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/README.md +42 -2
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/pyproject.toml +7 -2
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/__init__.py +1 -1
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/cli.py +95 -20
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/install.py +3 -0
- pisama_claude_code-0.4.2/src/pisama_claude_code/otel_export.py +287 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/workflows/ci.yml +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/workflows/publish.yml +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.gitignore +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/CODE_OF_CONDUCT.md +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/CONTRIBUTING.md +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/LICENSE +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/SECURITY.md +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/assets/demo.gif +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/demo/README.md +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/demo/demo-simulated.sh +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/demo/demo.cast +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/demo/record-demo.sh +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/adapter.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/guardian.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/hooks/__init__.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/hooks/capture_hook.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/hooks/guardian_hook.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/py.typed +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/storage.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/trace_converter.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/trace_types.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/__init__.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/conftest.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/test_adapter.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/test_cli.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/test_guardian.py +0 -0
- {pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/tests/test_storage.py +0 -0
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-01-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **OpenTelemetry export** - Export traces to any OTEL-compatible backend
|
|
14
|
+
- `export-otel` command for direct export to OTEL collectors (Jaeger, Honeycomb, Datadog)
|
|
15
|
+
- `export --format otel` for OTEL JSON file export
|
|
16
|
+
- GenAI semantic conventions for token usage and model attributes
|
|
17
|
+
- Optional `[otel]` dependency: `pip install pisama-claude-code[otel]`
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Updated description to reflect role in broader PISAMA platform
|
|
21
|
+
|
|
10
22
|
## [0.3.5] - 2025-01-05
|
|
11
23
|
|
|
12
24
|
### Fixed
|
|
@@ -91,7 +103,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
91
103
|
- Support for Bash, Read, Write, Edit, Grep, Glob tools
|
|
92
104
|
- Local storage in `~/.claude/pisama/traces/`
|
|
93
105
|
|
|
94
|
-
[Unreleased]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.
|
|
106
|
+
[Unreleased]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.4.0...HEAD
|
|
107
|
+
[0.4.0]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.5...v0.4.0
|
|
95
108
|
[0.3.5]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.4...v0.3.5
|
|
96
109
|
[0.3.4]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.3...v0.3.4
|
|
97
110
|
[0.3.3]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.2...v0.3.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pisama-claude-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Trace capture for Claude Code sessions - sync to PISAMA platform for analysis
|
|
5
5
|
Project-URL: Homepage, https://pisama.dev
|
|
6
6
|
Project-URL: Documentation, https://pisama.dev/docs/claude-code
|
|
@@ -36,6 +36,9 @@ Requires-Dist: httpx>=0.25.0
|
|
|
36
36
|
Requires-Dist: pydantic>=2.0.0
|
|
37
37
|
Requires-Dist: rich>=13.0.0
|
|
38
38
|
Provides-Extra: all
|
|
39
|
+
Requires-Dist: opentelemetry-api>=1.20.0; extra == 'all'
|
|
40
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0; extra == 'all'
|
|
41
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'all'
|
|
39
42
|
Requires-Dist: pisama-core>=0.1.0; extra == 'all'
|
|
40
43
|
Provides-Extra: core
|
|
41
44
|
Requires-Dist: pisama-core>=0.1.0; extra == 'core'
|
|
@@ -43,6 +46,10 @@ Provides-Extra: dev
|
|
|
43
46
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
44
47
|
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
45
48
|
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
49
|
+
Provides-Extra: otel
|
|
50
|
+
Requires-Dist: opentelemetry-api>=1.20.0; extra == 'otel'
|
|
51
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0; extra == 'otel'
|
|
52
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'otel'
|
|
46
53
|
Description-Content-Type: text/markdown
|
|
47
54
|
|
|
48
55
|
# pisama-claude-code
|
|
@@ -163,10 +170,34 @@ pisama-cc export -o traces.jsonl
|
|
|
163
170
|
# Export compressed
|
|
164
171
|
pisama-cc export -o traces.jsonl.gz --compress
|
|
165
172
|
|
|
173
|
+
# Export to OpenTelemetry format
|
|
174
|
+
pisama-cc export --format otel -o traces-otel.json
|
|
175
|
+
|
|
166
176
|
# Filter by date range
|
|
167
177
|
pisama-cc traces --since 2025-01-01 --until 2025-01-04
|
|
168
178
|
```
|
|
169
179
|
|
|
180
|
+
### OpenTelemetry Integration
|
|
181
|
+
|
|
182
|
+
Export traces to any OTEL-compatible backend (Jaeger, Honeycomb, Datadog, etc.):
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Install OTEL support
|
|
186
|
+
pip install pisama-claude-code[otel]
|
|
187
|
+
|
|
188
|
+
# Export to local Jaeger
|
|
189
|
+
pisama-cc export-otel -e http://localhost:4318/v1/traces
|
|
190
|
+
|
|
191
|
+
# Export to Honeycomb
|
|
192
|
+
pisama-cc export-otel -e https://api.honeycomb.io/v1/traces \
|
|
193
|
+
-H "x-honeycomb-team=YOUR_API_KEY"
|
|
194
|
+
|
|
195
|
+
# Export to file in OTEL format
|
|
196
|
+
pisama-cc export --format otel -o traces.json
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
OTEL export uses [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for token usage, costs, and model attributes.
|
|
200
|
+
|
|
170
201
|
## CLI Reference
|
|
171
202
|
|
|
172
203
|
| Command | Description |
|
|
@@ -174,12 +205,14 @@ pisama-cc traces --since 2025-01-01 --until 2025-01-04
|
|
|
174
205
|
| `pisama-cc install` | Install capture hooks to `~/.claude/hooks/` |
|
|
175
206
|
| `pisama-cc uninstall` | Remove hooks |
|
|
176
207
|
| `pisama-cc status` | Show status, token totals, and cost |
|
|
177
|
-
| `pisama-cc traces` | View recent traces (`-v` for verbose) |
|
|
208
|
+
| `pisama-cc traces` | View recent traces (`-v` for verbose, `-c` for content) |
|
|
178
209
|
| `pisama-cc usage` | Token usage breakdown (`--by-model`, `--by-tool`) |
|
|
179
|
-
| `pisama-cc export` | Export to JSONL (
|
|
210
|
+
| `pisama-cc export` | Export to JSONL or OTEL (`--format otel`, `--compress`) |
|
|
211
|
+
| `pisama-cc export-otel` | Export to OpenTelemetry collector (`-e ENDPOINT`) |
|
|
180
212
|
| `pisama-cc connect` | Connect to PISAMA platform (optional) |
|
|
181
213
|
| `pisama-cc sync` | Upload traces to platform |
|
|
182
214
|
| `pisama-cc analyze` | Run failure detection (requires platform) |
|
|
215
|
+
| `pisama-cc vault status` | Show PII tokenization vault status |
|
|
183
216
|
|
|
184
217
|
## Model Pricing
|
|
185
218
|
|
|
@@ -240,6 +273,20 @@ Platform features:
|
|
|
240
273
|
- Self-healing automation
|
|
241
274
|
- Visual dashboard
|
|
242
275
|
|
|
276
|
+
## Part of the PISAMA Platform
|
|
277
|
+
|
|
278
|
+
`pisama-claude-code` is the Claude Code integration for the broader **PISAMA (Platform for Intelligent Self-healing AI Multi-Agent) Testing Platform**, which supports multiple agent frameworks:
|
|
279
|
+
|
|
280
|
+
| Framework | Package | Status |
|
|
281
|
+
|-----------|---------|--------|
|
|
282
|
+
| Claude Code | `pisama-claude-code` | Stable |
|
|
283
|
+
| LangChain/LangGraph | `mao-testing` SDK | Available |
|
|
284
|
+
| CrewAI | `mao-testing` SDK | Available |
|
|
285
|
+
| AutoGen | `mao-testing` SDK | Available |
|
|
286
|
+
| n8n | `mao-testing` SDK | Available |
|
|
287
|
+
|
|
288
|
+
For other frameworks, see the [mao-testing SDK](https://github.com/tn-pisama/mao-testing).
|
|
289
|
+
|
|
243
290
|
## Contributing
|
|
244
291
|
|
|
245
292
|
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
@@ -116,10 +116,34 @@ pisama-cc export -o traces.jsonl
|
|
|
116
116
|
# Export compressed
|
|
117
117
|
pisama-cc export -o traces.jsonl.gz --compress
|
|
118
118
|
|
|
119
|
+
# Export to OpenTelemetry format
|
|
120
|
+
pisama-cc export --format otel -o traces-otel.json
|
|
121
|
+
|
|
119
122
|
# Filter by date range
|
|
120
123
|
pisama-cc traces --since 2025-01-01 --until 2025-01-04
|
|
121
124
|
```
|
|
122
125
|
|
|
126
|
+
### OpenTelemetry Integration
|
|
127
|
+
|
|
128
|
+
Export traces to any OTEL-compatible backend (Jaeger, Honeycomb, Datadog, etc.):
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Install OTEL support
|
|
132
|
+
pip install pisama-claude-code[otel]
|
|
133
|
+
|
|
134
|
+
# Export to local Jaeger
|
|
135
|
+
pisama-cc export-otel -e http://localhost:4318/v1/traces
|
|
136
|
+
|
|
137
|
+
# Export to Honeycomb
|
|
138
|
+
pisama-cc export-otel -e https://api.honeycomb.io/v1/traces \
|
|
139
|
+
-H "x-honeycomb-team=YOUR_API_KEY"
|
|
140
|
+
|
|
141
|
+
# Export to file in OTEL format
|
|
142
|
+
pisama-cc export --format otel -o traces.json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
OTEL export uses [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for token usage, costs, and model attributes.
|
|
146
|
+
|
|
123
147
|
## CLI Reference
|
|
124
148
|
|
|
125
149
|
| Command | Description |
|
|
@@ -127,12 +151,14 @@ pisama-cc traces --since 2025-01-01 --until 2025-01-04
|
|
|
127
151
|
| `pisama-cc install` | Install capture hooks to `~/.claude/hooks/` |
|
|
128
152
|
| `pisama-cc uninstall` | Remove hooks |
|
|
129
153
|
| `pisama-cc status` | Show status, token totals, and cost |
|
|
130
|
-
| `pisama-cc traces` | View recent traces (`-v` for verbose) |
|
|
154
|
+
| `pisama-cc traces` | View recent traces (`-v` for verbose, `-c` for content) |
|
|
131
155
|
| `pisama-cc usage` | Token usage breakdown (`--by-model`, `--by-tool`) |
|
|
132
|
-
| `pisama-cc export` | Export to JSONL (
|
|
156
|
+
| `pisama-cc export` | Export to JSONL or OTEL (`--format otel`, `--compress`) |
|
|
157
|
+
| `pisama-cc export-otel` | Export to OpenTelemetry collector (`-e ENDPOINT`) |
|
|
133
158
|
| `pisama-cc connect` | Connect to PISAMA platform (optional) |
|
|
134
159
|
| `pisama-cc sync` | Upload traces to platform |
|
|
135
160
|
| `pisama-cc analyze` | Run failure detection (requires platform) |
|
|
161
|
+
| `pisama-cc vault status` | Show PII tokenization vault status |
|
|
136
162
|
|
|
137
163
|
## Model Pricing
|
|
138
164
|
|
|
@@ -193,6 +219,20 @@ Platform features:
|
|
|
193
219
|
- Self-healing automation
|
|
194
220
|
- Visual dashboard
|
|
195
221
|
|
|
222
|
+
## Part of the PISAMA Platform
|
|
223
|
+
|
|
224
|
+
`pisama-claude-code` is the Claude Code integration for the broader **PISAMA (Platform for Intelligent Self-healing AI Multi-Agent) Testing Platform**, which supports multiple agent frameworks:
|
|
225
|
+
|
|
226
|
+
| Framework | Package | Status |
|
|
227
|
+
|-----------|---------|--------|
|
|
228
|
+
| Claude Code | `pisama-claude-code` | Stable |
|
|
229
|
+
| LangChain/LangGraph | `mao-testing` SDK | Available |
|
|
230
|
+
| CrewAI | `mao-testing` SDK | Available |
|
|
231
|
+
| AutoGen | `mao-testing` SDK | Available |
|
|
232
|
+
| n8n | `mao-testing` SDK | Available |
|
|
233
|
+
|
|
234
|
+
For other frameworks, see the [mao-testing SDK](https://github.com/tn-pisama/mao-testing).
|
|
235
|
+
|
|
196
236
|
## Contributing
|
|
197
237
|
|
|
198
238
|
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pisama-claude-code"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Trace capture for Claude Code sessions - sync to PISAMA platform for analysis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -62,13 +62,18 @@ dependencies = [
|
|
|
62
62
|
core = [
|
|
63
63
|
"pisama-core>=0.1.0",
|
|
64
64
|
]
|
|
65
|
+
otel = [
|
|
66
|
+
"opentelemetry-api>=1.20.0",
|
|
67
|
+
"opentelemetry-sdk>=1.20.0",
|
|
68
|
+
"opentelemetry-exporter-otlp-proto-http>=1.20.0",
|
|
69
|
+
]
|
|
65
70
|
dev = [
|
|
66
71
|
"pytest>=7.0.0",
|
|
67
72
|
"pytest-asyncio>=0.21.0",
|
|
68
73
|
"pytest-cov>=4.0.0",
|
|
69
74
|
]
|
|
70
75
|
all = [
|
|
71
|
-
"pisama-claude-code[core]",
|
|
76
|
+
"pisama-claude-code[core,otel]",
|
|
72
77
|
]
|
|
73
78
|
|
|
74
79
|
[project.scripts]
|
|
@@ -4,15 +4,20 @@ Command-line interface for capturing Claude Code traces and syncing
|
|
|
4
4
|
to the PISAMA platform for analysis and self-healing.
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
|
-
pisama-cc install
|
|
8
|
-
pisama-cc uninstall
|
|
9
|
-
pisama-cc status
|
|
10
|
-
pisama-cc traces
|
|
11
|
-
pisama-cc usage
|
|
12
|
-
pisama-cc export
|
|
13
|
-
pisama-cc
|
|
14
|
-
pisama-cc
|
|
15
|
-
pisama-cc
|
|
7
|
+
pisama-cc install Install hooks to ~/.claude/
|
|
8
|
+
pisama-cc uninstall Remove hooks
|
|
9
|
+
pisama-cc status Show current status (incl. token/cost totals)
|
|
10
|
+
pisama-cc traces View recent traces (-v for token usage)
|
|
11
|
+
pisama-cc usage Show token usage and cost breakdown
|
|
12
|
+
pisama-cc export Export traces to file (JSONL or OTEL format)
|
|
13
|
+
pisama-cc export-otel Export traces to OpenTelemetry collector
|
|
14
|
+
pisama-cc connect Connect to PISAMA platform
|
|
15
|
+
pisama-cc sync Sync traces to platform
|
|
16
|
+
pisama-cc analyze Analyze traces (requires platform)
|
|
17
|
+
|
|
18
|
+
OTEL Export:
|
|
19
|
+
pisama-cc export --format otel -o traces.json
|
|
20
|
+
pisama-cc export-otel -e http://localhost:4318/v1/traces
|
|
16
21
|
"""
|
|
17
22
|
|
|
18
23
|
import click
|
|
@@ -53,7 +58,7 @@ def save_config(config: dict):
|
|
|
53
58
|
|
|
54
59
|
|
|
55
60
|
@click.group()
|
|
56
|
-
@click.version_option(version="0.
|
|
61
|
+
@click.version_option(version="0.4.0")
|
|
57
62
|
def main():
|
|
58
63
|
"""PISAMA Claude Code - Trace capture and sync."""
|
|
59
64
|
pass
|
|
@@ -69,13 +74,6 @@ def install(force: bool):
|
|
|
69
74
|
from pisama_claude_code.install import install as do_install
|
|
70
75
|
do_install(force=force)
|
|
71
76
|
|
|
72
|
-
# Star reminder after install
|
|
73
|
-
click.echo("")
|
|
74
|
-
click.echo("=" * 50)
|
|
75
|
-
click.echo("If pisama-claude-code is useful, please star us!")
|
|
76
|
-
click.echo(f" {GITHUB_URL}")
|
|
77
|
-
click.echo("=" * 50)
|
|
78
|
-
|
|
79
77
|
|
|
80
78
|
@main.command()
|
|
81
79
|
def uninstall():
|
|
@@ -515,15 +513,29 @@ def usage(last: int, by_model: bool, by_tool: bool):
|
|
|
515
513
|
@click.option("--last", default=50, help="Number of traces to export")
|
|
516
514
|
@click.option("--output", "-o", default="traces-export.jsonl", help="Output file")
|
|
517
515
|
@click.option("--compress", is_flag=True, help="Gzip compress output")
|
|
518
|
-
|
|
519
|
-
|
|
516
|
+
@click.option("--format", "fmt", type=click.Choice(["jsonl", "otel"]), default="jsonl", help="Export format")
|
|
517
|
+
def export(last: int, output: str, compress: bool, fmt: str):
|
|
518
|
+
"""Export traces to a file (JSONL or OTEL format)."""
|
|
520
519
|
traces_list = load_recent_traces(last)
|
|
521
520
|
|
|
522
521
|
if not traces_list:
|
|
523
522
|
click.echo("No traces to export")
|
|
524
523
|
return
|
|
525
524
|
|
|
526
|
-
#
|
|
525
|
+
# Handle OTEL format
|
|
526
|
+
if fmt == "otel":
|
|
527
|
+
from pisama_claude_code.otel_export import export_to_otel_file
|
|
528
|
+
|
|
529
|
+
# Adjust default output for OTEL
|
|
530
|
+
if output == "traces-export.jsonl":
|
|
531
|
+
output = "traces-export-otel.json"
|
|
532
|
+
|
|
533
|
+
result = export_to_otel_file(traces_list, output)
|
|
534
|
+
click.echo(f"✅ Exported {result['spans_created']} spans to {output} (OTEL format)")
|
|
535
|
+
click.echo(f" Sessions: {result['sessions_exported']}")
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
# Prepare JSONL export (redact sensitive data)
|
|
527
539
|
export_data = []
|
|
528
540
|
for t in traces_list:
|
|
529
541
|
clean = {
|
|
@@ -572,6 +584,69 @@ def export(last: int, output: str, compress: bool):
|
|
|
572
584
|
click.echo(f"✅ Exported {len(export_data)} traces to {output_path} ({size_kb} KB)")
|
|
573
585
|
|
|
574
586
|
|
|
587
|
+
@main.command("export-otel")
|
|
588
|
+
@click.option("--last", default=50, help="Number of traces to export")
|
|
589
|
+
@click.option("--endpoint", "-e", required=True, help="OTEL collector endpoint (e.g., http://localhost:4318/v1/traces)")
|
|
590
|
+
@click.option("--service-name", "-s", default="claude-code", help="Service name for spans")
|
|
591
|
+
@click.option("--header", "-H", multiple=True, help="Header in 'key=value' format (can be repeated)")
|
|
592
|
+
def export_otel(last: int, endpoint: str, service_name: str, header: tuple):
|
|
593
|
+
"""Export traces to an OpenTelemetry collector.
|
|
594
|
+
|
|
595
|
+
Examples:
|
|
596
|
+
|
|
597
|
+
# Local Jaeger
|
|
598
|
+
pisama-cc export-otel -e http://localhost:4318/v1/traces
|
|
599
|
+
|
|
600
|
+
# Honeycomb
|
|
601
|
+
pisama-cc export-otel -e https://api.honeycomb.io/v1/traces \\
|
|
602
|
+
-H "x-honeycomb-team=YOUR_API_KEY"
|
|
603
|
+
|
|
604
|
+
# Datadog (via OTEL collector)
|
|
605
|
+
pisama-cc export-otel -e http://localhost:4318/v1/traces \\
|
|
606
|
+
-s my-claude-agent
|
|
607
|
+
"""
|
|
608
|
+
from pisama_claude_code.otel_export import is_otel_available, export_traces_to_otel
|
|
609
|
+
|
|
610
|
+
if not is_otel_available():
|
|
611
|
+
click.echo("❌ OpenTelemetry not installed")
|
|
612
|
+
click.echo(" Run: pip install pisama-claude-code[otel]")
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
traces_list = load_recent_traces(last)
|
|
616
|
+
|
|
617
|
+
if not traces_list:
|
|
618
|
+
click.echo("No traces to export")
|
|
619
|
+
return
|
|
620
|
+
|
|
621
|
+
# Parse headers
|
|
622
|
+
headers = {}
|
|
623
|
+
for h in header:
|
|
624
|
+
if "=" in h:
|
|
625
|
+
key, value = h.split("=", 1)
|
|
626
|
+
headers[key.strip()] = value.strip()
|
|
627
|
+
|
|
628
|
+
click.echo(f"📤 Exporting {len(traces_list)} traces to OTEL collector...")
|
|
629
|
+
click.echo(f" Endpoint: {endpoint}")
|
|
630
|
+
click.echo(f" Service: {service_name}")
|
|
631
|
+
|
|
632
|
+
try:
|
|
633
|
+
result = export_traces_to_otel(
|
|
634
|
+
traces=traces_list,
|
|
635
|
+
endpoint=endpoint,
|
|
636
|
+
service_name=service_name,
|
|
637
|
+
headers=headers if headers else None,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
if result["success"]:
|
|
641
|
+
click.echo(f"✅ Exported {result['spans_created']} spans")
|
|
642
|
+
click.echo(f" Sessions: {result['sessions_exported']}")
|
|
643
|
+
click.echo(f" Traces: {result['traces_processed']}")
|
|
644
|
+
else:
|
|
645
|
+
click.echo("❌ Export failed")
|
|
646
|
+
except Exception as e:
|
|
647
|
+
click.echo(f"❌ Export failed: {e}")
|
|
648
|
+
|
|
649
|
+
|
|
575
650
|
# Helper functions
|
|
576
651
|
|
|
577
652
|
def normalize_trace(t: dict) -> dict:
|
|
@@ -74,6 +74,9 @@ def install(force: bool = False):
|
|
|
74
74
|
print("\nNext steps:")
|
|
75
75
|
print(" 1. Add hooks to settings.local.json (see above)")
|
|
76
76
|
print(" 2. Run 'pisama-cc connect --api-key <key>' to enable analysis")
|
|
77
|
+
print("\n" + "─" * 50)
|
|
78
|
+
print("⭐ If this tool saves you time/money, consider starring:")
|
|
79
|
+
print(" https://github.com/tn-pisama/pisama-claude-code")
|
|
77
80
|
|
|
78
81
|
|
|
79
82
|
def _install_shell_hooks(hooks_dir: Path, force: bool):
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""OpenTelemetry export for pisama-claude-code traces.
|
|
2
|
+
|
|
3
|
+
Converts Claude Code traces to OpenTelemetry spans and exports them
|
|
4
|
+
to any OTEL-compatible backend (Jaeger, Honeycomb, Datadog, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
# Lazy imports for OTEL
|
|
14
|
+
_otel_available = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_otel_available() -> bool:
|
|
18
|
+
"""Check if OpenTelemetry is installed."""
|
|
19
|
+
global _otel_available
|
|
20
|
+
if _otel_available is None:
|
|
21
|
+
try:
|
|
22
|
+
import opentelemetry # noqa: F401
|
|
23
|
+
_otel_available = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
_otel_available = False
|
|
26
|
+
return _otel_available
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def export_traces_to_otel(
|
|
30
|
+
traces: List[Dict[str, Any]],
|
|
31
|
+
endpoint: str,
|
|
32
|
+
service_name: str = "claude-code",
|
|
33
|
+
headers: Optional[Dict[str, str]] = None,
|
|
34
|
+
) -> Dict[str, Any]:
|
|
35
|
+
"""Export traces to an OTEL endpoint.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
traces: List of normalized trace dicts from pisama-claude-code
|
|
39
|
+
endpoint: OTEL collector endpoint (e.g., http://localhost:4318/v1/traces)
|
|
40
|
+
service_name: Service name for the spans
|
|
41
|
+
headers: Optional headers (e.g., for authentication)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dict with export status and statistics
|
|
45
|
+
"""
|
|
46
|
+
if not is_otel_available():
|
|
47
|
+
raise ImportError(
|
|
48
|
+
"OpenTelemetry not installed. Run: pip install pisama-claude-code[otel]"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from opentelemetry import trace as otel_trace
|
|
52
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
53
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
54
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
55
|
+
from opentelemetry.sdk.resources import Resource
|
|
56
|
+
from opentelemetry.trace import SpanKind, Status, StatusCode
|
|
57
|
+
|
|
58
|
+
# Set up resource
|
|
59
|
+
resource = Resource.create({
|
|
60
|
+
"service.name": service_name,
|
|
61
|
+
"service.version": "0.3.7",
|
|
62
|
+
"telemetry.sdk.name": "pisama-claude-code",
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
# Create provider and exporter
|
|
66
|
+
provider = TracerProvider(resource=resource)
|
|
67
|
+
exporter = OTLPSpanExporter(
|
|
68
|
+
endpoint=endpoint,
|
|
69
|
+
headers=headers or {},
|
|
70
|
+
)
|
|
71
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
72
|
+
otel_trace.set_tracer_provider(provider)
|
|
73
|
+
|
|
74
|
+
tracer = otel_trace.get_tracer("pisama-claude-code")
|
|
75
|
+
|
|
76
|
+
# Group traces by session
|
|
77
|
+
sessions: Dict[str, List[Dict]] = {}
|
|
78
|
+
for t in traces:
|
|
79
|
+
session_id = t.get("session_id", "unknown")
|
|
80
|
+
if session_id not in sessions:
|
|
81
|
+
sessions[session_id] = []
|
|
82
|
+
sessions[session_id].append(t)
|
|
83
|
+
|
|
84
|
+
spans_created = 0
|
|
85
|
+
|
|
86
|
+
# Create spans for each session
|
|
87
|
+
for session_id, session_traces in sessions.items():
|
|
88
|
+
# Sort by timestamp
|
|
89
|
+
session_traces.sort(key=lambda x: x.get("timestamp", ""))
|
|
90
|
+
|
|
91
|
+
# Create parent span for session
|
|
92
|
+
session_start = _parse_timestamp(session_traces[0].get("timestamp"))
|
|
93
|
+
session_end = _parse_timestamp(session_traces[-1].get("timestamp"))
|
|
94
|
+
|
|
95
|
+
with tracer.start_span(
|
|
96
|
+
name=f"claude-code-session:{session_id}",
|
|
97
|
+
kind=SpanKind.INTERNAL,
|
|
98
|
+
start_time=session_start,
|
|
99
|
+
) as session_span:
|
|
100
|
+
session_span.set_attribute("session.id", session_id)
|
|
101
|
+
session_span.set_attribute("session.trace_count", len(session_traces))
|
|
102
|
+
|
|
103
|
+
# Calculate session totals
|
|
104
|
+
total_input = sum(t.get("input_tokens", 0) for t in session_traces)
|
|
105
|
+
total_output = sum(t.get("output_tokens", 0) for t in session_traces)
|
|
106
|
+
total_cost = sum(t.get("cost_usd", 0) for t in session_traces)
|
|
107
|
+
|
|
108
|
+
session_span.set_attribute("session.input_tokens", total_input)
|
|
109
|
+
session_span.set_attribute("session.output_tokens", total_output)
|
|
110
|
+
session_span.set_attribute("session.cost_usd", total_cost)
|
|
111
|
+
|
|
112
|
+
spans_created += 1
|
|
113
|
+
|
|
114
|
+
# Create child spans for each tool call
|
|
115
|
+
for t in session_traces:
|
|
116
|
+
span_start = _parse_timestamp(t.get("timestamp"))
|
|
117
|
+
tool_name = t.get("tool_name", "unknown")
|
|
118
|
+
hook_type = t.get("hook_type", "")
|
|
119
|
+
|
|
120
|
+
with tracer.start_span(
|
|
121
|
+
name=f"{tool_name}:{hook_type}",
|
|
122
|
+
kind=SpanKind.INTERNAL,
|
|
123
|
+
start_time=span_start,
|
|
124
|
+
) as span:
|
|
125
|
+
# Standard attributes
|
|
126
|
+
span.set_attribute("tool.name", tool_name)
|
|
127
|
+
span.set_attribute("hook.type", hook_type)
|
|
128
|
+
span.set_attribute("session.id", session_id)
|
|
129
|
+
|
|
130
|
+
# Model and token usage (GenAI semantic conventions)
|
|
131
|
+
if t.get("model"):
|
|
132
|
+
span.set_attribute("gen_ai.system", "anthropic")
|
|
133
|
+
span.set_attribute("gen_ai.request.model", t["model"])
|
|
134
|
+
|
|
135
|
+
if t.get("input_tokens"):
|
|
136
|
+
span.set_attribute("gen_ai.usage.input_tokens", t["input_tokens"])
|
|
137
|
+
if t.get("output_tokens"):
|
|
138
|
+
span.set_attribute("gen_ai.usage.output_tokens", t["output_tokens"])
|
|
139
|
+
if t.get("cache_read_tokens"):
|
|
140
|
+
span.set_attribute("gen_ai.usage.cache_read_tokens", t["cache_read_tokens"])
|
|
141
|
+
if t.get("cost_usd"):
|
|
142
|
+
span.set_attribute("gen_ai.usage.cost_usd", t["cost_usd"])
|
|
143
|
+
|
|
144
|
+
# Content attributes (if captured)
|
|
145
|
+
if t.get("user_input"):
|
|
146
|
+
span.set_attribute("gen_ai.prompt", _truncate(t["user_input"], 4096))
|
|
147
|
+
if t.get("reasoning"):
|
|
148
|
+
span.set_attribute("gen_ai.reasoning", _truncate(t["reasoning"], 4096))
|
|
149
|
+
if t.get("ai_output"):
|
|
150
|
+
span.set_attribute("gen_ai.completion", _truncate(t["ai_output"], 4096))
|
|
151
|
+
|
|
152
|
+
# Tool input (sanitized)
|
|
153
|
+
tool_input = t.get("tool_input")
|
|
154
|
+
if tool_input:
|
|
155
|
+
try:
|
|
156
|
+
span.set_attribute("tool.input", json.dumps(tool_input)[:2048])
|
|
157
|
+
except (TypeError, ValueError):
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
# Working directory
|
|
161
|
+
if t.get("working_dir"):
|
|
162
|
+
span.set_attribute("tool.working_dir", t["working_dir"])
|
|
163
|
+
|
|
164
|
+
spans_created += 1
|
|
165
|
+
|
|
166
|
+
# Force flush and shutdown
|
|
167
|
+
provider.force_flush()
|
|
168
|
+
provider.shutdown()
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
"success": True,
|
|
172
|
+
"endpoint": endpoint,
|
|
173
|
+
"service_name": service_name,
|
|
174
|
+
"sessions_exported": len(sessions),
|
|
175
|
+
"spans_created": spans_created,
|
|
176
|
+
"traces_processed": len(traces),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def convert_trace_to_otel_dict(trace: Dict[str, Any]) -> Dict[str, Any]:
|
|
181
|
+
"""Convert a single trace to OTEL span format (dict).
|
|
182
|
+
|
|
183
|
+
Useful for exporting to file in OTEL-compatible format.
|
|
184
|
+
"""
|
|
185
|
+
timestamp_ns = _parse_timestamp(trace.get("timestamp"))
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
"name": f"{trace.get('tool_name', 'unknown')}:{trace.get('hook_type', '')}",
|
|
189
|
+
"kind": "SPAN_KIND_INTERNAL",
|
|
190
|
+
"startTimeUnixNano": timestamp_ns,
|
|
191
|
+
"endTimeUnixNano": timestamp_ns + 1_000_000, # +1ms
|
|
192
|
+
"attributes": [
|
|
193
|
+
{"key": "tool.name", "value": {"stringValue": trace.get("tool_name", "")}},
|
|
194
|
+
{"key": "hook.type", "value": {"stringValue": trace.get("hook_type", "")}},
|
|
195
|
+
{"key": "session.id", "value": {"stringValue": trace.get("session_id", "")}},
|
|
196
|
+
{"key": "gen_ai.system", "value": {"stringValue": "anthropic"}},
|
|
197
|
+
{"key": "gen_ai.request.model", "value": {"stringValue": trace.get("model", "")}},
|
|
198
|
+
{"key": "gen_ai.usage.input_tokens", "value": {"intValue": trace.get("input_tokens", 0)}},
|
|
199
|
+
{"key": "gen_ai.usage.output_tokens", "value": {"intValue": trace.get("output_tokens", 0)}},
|
|
200
|
+
],
|
|
201
|
+
"traceId": _generate_trace_id(trace.get("session_id", "")),
|
|
202
|
+
"spanId": _generate_span_id(trace.get("timestamp", "")),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def export_to_otel_file(
|
|
207
|
+
traces: List[Dict[str, Any]],
|
|
208
|
+
output_path: str,
|
|
209
|
+
service_name: str = "claude-code",
|
|
210
|
+
) -> Dict[str, Any]:
|
|
211
|
+
"""Export traces to an OTEL-compatible JSON file.
|
|
212
|
+
|
|
213
|
+
The output format follows the OTLP JSON schema.
|
|
214
|
+
"""
|
|
215
|
+
resource_spans = []
|
|
216
|
+
|
|
217
|
+
# Group by session
|
|
218
|
+
sessions: Dict[str, List[Dict]] = {}
|
|
219
|
+
for t in traces:
|
|
220
|
+
session_id = t.get("session_id", "unknown")
|
|
221
|
+
if session_id not in sessions:
|
|
222
|
+
sessions[session_id] = []
|
|
223
|
+
sessions[session_id].append(t)
|
|
224
|
+
|
|
225
|
+
for session_id, session_traces in sessions.items():
|
|
226
|
+
spans = [convert_trace_to_otel_dict(t) for t in session_traces]
|
|
227
|
+
|
|
228
|
+
resource_spans.append({
|
|
229
|
+
"resource": {
|
|
230
|
+
"attributes": [
|
|
231
|
+
{"key": "service.name", "value": {"stringValue": service_name}},
|
|
232
|
+
{"key": "session.id", "value": {"stringValue": session_id}},
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
"scopeSpans": [
|
|
236
|
+
{
|
|
237
|
+
"scope": {"name": "pisama-claude-code"},
|
|
238
|
+
"spans": spans,
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
otel_payload = {"resourceSpans": resource_spans}
|
|
244
|
+
|
|
245
|
+
with open(output_path, "w") as f:
|
|
246
|
+
json.dump(otel_payload, f, indent=2)
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"success": True,
|
|
250
|
+
"output_path": output_path,
|
|
251
|
+
"sessions_exported": len(sessions),
|
|
252
|
+
"spans_created": sum(len(s) for s in sessions.values()),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _parse_timestamp(ts: Optional[str]) -> int:
|
|
257
|
+
"""Parse timestamp string to nanoseconds since epoch."""
|
|
258
|
+
if not ts:
|
|
259
|
+
return time.time_ns()
|
|
260
|
+
try:
|
|
261
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
262
|
+
return int(dt.timestamp() * 1_000_000_000)
|
|
263
|
+
except (ValueError, TypeError):
|
|
264
|
+
return time.time_ns()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _truncate(text: str, max_len: int) -> str:
|
|
268
|
+
"""Truncate text to max length."""
|
|
269
|
+
if not text:
|
|
270
|
+
return ""
|
|
271
|
+
if len(text) > max_len:
|
|
272
|
+
return text[:max_len - 3] + "..."
|
|
273
|
+
return text
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _generate_trace_id(session_id: str) -> str:
|
|
277
|
+
"""Generate a 32-char hex trace ID from session ID."""
|
|
278
|
+
import hashlib
|
|
279
|
+
h = hashlib.sha256(session_id.encode()).hexdigest()
|
|
280
|
+
return h[:32]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _generate_span_id(identifier: str) -> str:
|
|
284
|
+
"""Generate a 16-char hex span ID."""
|
|
285
|
+
import hashlib
|
|
286
|
+
h = hashlib.sha256(identifier.encode()).hexdigest()
|
|
287
|
+
return h[:16]
|
|
File without changes
|
|
File without changes
|
{pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/.github/ISSUE_TEMPLATE/feature_request.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/hooks/__init__.py
RENAMED
|
File without changes
|
{pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/hooks/capture_hook.py
RENAMED
|
File without changes
|
{pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/hooks/guardian_hook.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pisama_claude_code-0.3.7 → pisama_claude_code-0.4.2}/src/pisama_claude_code/trace_converter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|