claudetracing 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.
- claudetracing-0.1.0/.gitignore +29 -0
- claudetracing-0.1.0/.python-version +1 -0
- claudetracing-0.1.0/LICENSE +21 -0
- claudetracing-0.1.0/PKG-INFO +139 -0
- claudetracing-0.1.0/README.md +112 -0
- claudetracing-0.1.0/pyproject.toml +54 -0
- claudetracing-0.1.0/src/claudetracing/__init__.py +30 -0
- claudetracing-0.1.0/src/claudetracing/cli.py +93 -0
- claudetracing-0.1.0/src/claudetracing/client.py +184 -0
- claudetracing-0.1.0/src/claudetracing/formatters.py +167 -0
- claudetracing-0.1.0/src/claudetracing/models.py +87 -0
- claudetracing-0.1.0/src/claudetracing/setup.py +339 -0
- claudetracing-0.1.0/tests/test_setup.py +204 -0
- claudetracing-0.1.0/uv.lock +4726 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# macOS
|
|
2
|
+
.DS_Store
|
|
3
|
+
|
|
4
|
+
# Python-generated files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[oc]
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
wheels/
|
|
10
|
+
*.egg-info
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv
|
|
14
|
+
.env
|
|
15
|
+
|
|
16
|
+
# Databricks
|
|
17
|
+
.databrickscfg
|
|
18
|
+
|
|
19
|
+
# MLflow
|
|
20
|
+
mlruns/
|
|
21
|
+
mlflow.db
|
|
22
|
+
|
|
23
|
+
# Claude Code
|
|
24
|
+
.claude/
|
|
25
|
+
|
|
26
|
+
# Caches
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.ruff_cache/
|
|
29
|
+
.mypy_cache/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.10
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Casper
|
|
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,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claudetracing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MLflow tracing for Claude Code sessions with Databricks integration
|
|
5
|
+
Project-URL: Homepage, https://github.com/CauchyIO/claudetracing
|
|
6
|
+
Project-URL: Repository, https://github.com/CauchyIO/claudetracing
|
|
7
|
+
Project-URL: Issues, https://github.com/CauchyIO/claudetracing/issues
|
|
8
|
+
Author: Casper
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: claude,databricks,llm,mlflow,tracing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: mlflow[databricks]>=3.4
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Requires-Dist: ty>=0.0.8
|
|
25
|
+
Requires-Dist: typer>=0.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Claude Code MLflow Tracing
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/claudetracing/)
|
|
31
|
+
[](https://pypi.org/project/claudetracing/)
|
|
32
|
+
[](https://opensource.org/licenses/MIT)
|
|
33
|
+
|
|
34
|
+
MLflow tracing for Claude Code sessions with Databricks integration. Automatically captures conversations, tool usage, and session metadata.
|
|
35
|
+
|
|
36
|
+
## Why Trace Claude Code Sessions?
|
|
37
|
+
|
|
38
|
+
When Claude Code becomes part of your development workflow, visibility into how it's being used becomes valuable:
|
|
39
|
+
|
|
40
|
+
- **Review past sessions** - What did Claude do while you were away? Search and replay any session to understand decisions made.
|
|
41
|
+
- **Team insights** - See how your team uses Claude Code across projects. Identify patterns, common tasks, and areas for improvement.
|
|
42
|
+
- **Debug failures** - When something goes wrong, trace data shows exactly which tools were called, in what order, and what inputs/outputs were involved.
|
|
43
|
+
- **Cost awareness** - Track token usage and session duration to understand resource consumption.
|
|
44
|
+
- **Compliance & audit** - Maintain records of AI-assisted code changes for regulated environments.
|
|
45
|
+
|
|
46
|
+
## Prerequisites
|
|
47
|
+
|
|
48
|
+
- Python 3.10+
|
|
49
|
+
- [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) installed
|
|
50
|
+
- Access to a Databricks workspace
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv add claudetracing
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or with pip:
|
|
59
|
+
```bash
|
|
60
|
+
pip install claudetracing
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
Run the interactive setup in your project directory:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
traces init
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This will:
|
|
72
|
+
1. Authenticate with Databricks (or use existing credentials)
|
|
73
|
+
2. Configure your experiment path (shared or personal)
|
|
74
|
+
3. Create `.claude/settings.json` with the proper hooks
|
|
75
|
+
4. Update `.gitignore`
|
|
76
|
+
|
|
77
|
+
Restart Claude Code after setup. Traces are automatically sent to Databricks when sessions end.
|
|
78
|
+
|
|
79
|
+
## CLI Commands
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
traces init # Interactive setup
|
|
83
|
+
traces list # List available experiments
|
|
84
|
+
traces search # Search recent traces
|
|
85
|
+
traces search -e <experiment> # Filter by experiment name
|
|
86
|
+
traces search --hours 24 # Last 24 hours
|
|
87
|
+
traces search --trace-id <id> # Get specific trace
|
|
88
|
+
traces search -f json # Output as JSON
|
|
89
|
+
traces search -f context # LLM-optimized format
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Python API
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from claudetracing import TracingClient
|
|
96
|
+
|
|
97
|
+
client = TracingClient()
|
|
98
|
+
|
|
99
|
+
# List experiments
|
|
100
|
+
experiments = client.list_experiments()
|
|
101
|
+
|
|
102
|
+
# Search traces
|
|
103
|
+
traces = client.search_traces(experiment_name="my-experiment", max_results=10)
|
|
104
|
+
|
|
105
|
+
# Search by time
|
|
106
|
+
traces = client.search_traces_by_time(hours=24)
|
|
107
|
+
|
|
108
|
+
# Get specific trace
|
|
109
|
+
trace = client.get_trace("trace-id")
|
|
110
|
+
|
|
111
|
+
# Access trace data
|
|
112
|
+
for trace in traces:
|
|
113
|
+
print(f"Session: {trace.info.trace_id}")
|
|
114
|
+
print(f"Tools used: {trace.get_tool_calls()}")
|
|
115
|
+
for span in trace.spans:
|
|
116
|
+
print(f" {span.name}: {span.execution_time_ms}ms")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## What Gets Traced
|
|
120
|
+
|
|
121
|
+
- User prompts and Claude responses
|
|
122
|
+
- Tool usage (Read, Write, Edit, Bash, etc.)
|
|
123
|
+
- Execution timing per operation
|
|
124
|
+
- Session metadata (user, working directory, git branch)
|
|
125
|
+
|
|
126
|
+
## How It Works
|
|
127
|
+
|
|
128
|
+
1. The `traces init` command creates a `.claude/settings.json` file
|
|
129
|
+
2. This configures a Stop hook that runs when Claude Code sessions end
|
|
130
|
+
3. The hook calls MLflow's built-in Claude Code tracing to capture the session
|
|
131
|
+
4. Traces are uploaded to your Databricks MLflow experiment
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
Built with [Claude Opus 4.5](https://www.anthropic.com/claude)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Claude Code MLflow Tracing
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/claudetracing/)
|
|
4
|
+
[](https://pypi.org/project/claudetracing/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
MLflow tracing for Claude Code sessions with Databricks integration. Automatically captures conversations, tool usage, and session metadata.
|
|
8
|
+
|
|
9
|
+
## Why Trace Claude Code Sessions?
|
|
10
|
+
|
|
11
|
+
When Claude Code becomes part of your development workflow, visibility into how it's being used becomes valuable:
|
|
12
|
+
|
|
13
|
+
- **Review past sessions** - What did Claude do while you were away? Search and replay any session to understand decisions made.
|
|
14
|
+
- **Team insights** - See how your team uses Claude Code across projects. Identify patterns, common tasks, and areas for improvement.
|
|
15
|
+
- **Debug failures** - When something goes wrong, trace data shows exactly which tools were called, in what order, and what inputs/outputs were involved.
|
|
16
|
+
- **Cost awareness** - Track token usage and session duration to understand resource consumption.
|
|
17
|
+
- **Compliance & audit** - Maintain records of AI-assisted code changes for regulated environments.
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
- Python 3.10+
|
|
22
|
+
- [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) installed
|
|
23
|
+
- Access to a Databricks workspace
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv add claudetracing
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or with pip:
|
|
32
|
+
```bash
|
|
33
|
+
pip install claudetracing
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
Run the interactive setup in your project directory:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
traces init
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This will:
|
|
45
|
+
1. Authenticate with Databricks (or use existing credentials)
|
|
46
|
+
2. Configure your experiment path (shared or personal)
|
|
47
|
+
3. Create `.claude/settings.json` with the proper hooks
|
|
48
|
+
4. Update `.gitignore`
|
|
49
|
+
|
|
50
|
+
Restart Claude Code after setup. Traces are automatically sent to Databricks when sessions end.
|
|
51
|
+
|
|
52
|
+
## CLI Commands
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
traces init # Interactive setup
|
|
56
|
+
traces list # List available experiments
|
|
57
|
+
traces search # Search recent traces
|
|
58
|
+
traces search -e <experiment> # Filter by experiment name
|
|
59
|
+
traces search --hours 24 # Last 24 hours
|
|
60
|
+
traces search --trace-id <id> # Get specific trace
|
|
61
|
+
traces search -f json # Output as JSON
|
|
62
|
+
traces search -f context # LLM-optimized format
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Python API
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from claudetracing import TracingClient
|
|
69
|
+
|
|
70
|
+
client = TracingClient()
|
|
71
|
+
|
|
72
|
+
# List experiments
|
|
73
|
+
experiments = client.list_experiments()
|
|
74
|
+
|
|
75
|
+
# Search traces
|
|
76
|
+
traces = client.search_traces(experiment_name="my-experiment", max_results=10)
|
|
77
|
+
|
|
78
|
+
# Search by time
|
|
79
|
+
traces = client.search_traces_by_time(hours=24)
|
|
80
|
+
|
|
81
|
+
# Get specific trace
|
|
82
|
+
trace = client.get_trace("trace-id")
|
|
83
|
+
|
|
84
|
+
# Access trace data
|
|
85
|
+
for trace in traces:
|
|
86
|
+
print(f"Session: {trace.info.trace_id}")
|
|
87
|
+
print(f"Tools used: {trace.get_tool_calls()}")
|
|
88
|
+
for span in trace.spans:
|
|
89
|
+
print(f" {span.name}: {span.execution_time_ms}ms")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## What Gets Traced
|
|
93
|
+
|
|
94
|
+
- User prompts and Claude responses
|
|
95
|
+
- Tool usage (Read, Write, Edit, Bash, etc.)
|
|
96
|
+
- Execution timing per operation
|
|
97
|
+
- Session metadata (user, working directory, git branch)
|
|
98
|
+
|
|
99
|
+
## How It Works
|
|
100
|
+
|
|
101
|
+
1. The `traces init` command creates a `.claude/settings.json` file
|
|
102
|
+
2. This configures a Stop hook that runs when Claude Code sessions end
|
|
103
|
+
3. The hook calls MLflow's built-in Claude Code tracing to capture the session
|
|
104
|
+
4. Traces are uploaded to your Databricks MLflow experiment
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
Built with [Claude Opus 4.5](https://www.anthropic.com/claude)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "claudetracing"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MLflow tracing for Claude Code sessions with Databricks integration"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Casper" }]
|
|
9
|
+
keywords = ["claude", "mlflow", "tracing", "databricks", "llm"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"mlflow[databricks]>=3.4",
|
|
23
|
+
"pydantic>=2.0",
|
|
24
|
+
"ty>=0.0.8",
|
|
25
|
+
"typer>=0.9",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/CauchyIO/claudetracing"
|
|
30
|
+
Repository = "https://github.com/CauchyIO/claudetracing"
|
|
31
|
+
Issues = "https://github.com/CauchyIO/claudetracing/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
traces = "claudetracing.cli:main"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/claudetracing"]
|
|
42
|
+
|
|
43
|
+
[tool.uv]
|
|
44
|
+
package = true
|
|
45
|
+
|
|
46
|
+
[dependency-groups]
|
|
47
|
+
dev = [
|
|
48
|
+
"pytest>=9.0.2",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
filterwarnings = [
|
|
53
|
+
"ignore:The distutils package is deprecated:DeprecationWarning",
|
|
54
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Claude Code Tracing - MLflow tracing for Claude Code sessions."""
|
|
2
|
+
|
|
3
|
+
from .client import TracingClient
|
|
4
|
+
from .formatters import (
|
|
5
|
+
format_for_context,
|
|
6
|
+
format_tool_usage,
|
|
7
|
+
format_traces_json,
|
|
8
|
+
format_traces_summary,
|
|
9
|
+
to_json,
|
|
10
|
+
to_summary,
|
|
11
|
+
)
|
|
12
|
+
from .models import SpanInfo, TraceData, TraceInfo, TraceSummary
|
|
13
|
+
from .setup import load_settings
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"TracingClient",
|
|
17
|
+
"load_settings",
|
|
18
|
+
"TraceData",
|
|
19
|
+
"TraceInfo",
|
|
20
|
+
"TraceSummary",
|
|
21
|
+
"SpanInfo",
|
|
22
|
+
"to_summary",
|
|
23
|
+
"to_json",
|
|
24
|
+
"format_traces_summary",
|
|
25
|
+
"format_traces_json",
|
|
26
|
+
"format_for_context",
|
|
27
|
+
"format_tool_usage",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""CLI entry point for trace retrieval."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from .client import TracingClient
|
|
9
|
+
from .formatters import (
|
|
10
|
+
format_for_context,
|
|
11
|
+
format_tool_usage,
|
|
12
|
+
format_traces_json,
|
|
13
|
+
format_traces_summary,
|
|
14
|
+
)
|
|
15
|
+
from .setup import load_settings
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(help="Claude Code MLflow tracing CLI")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def init():
|
|
22
|
+
"""Initialize Claude Code tracing in the current project."""
|
|
23
|
+
from .setup import run_setup
|
|
24
|
+
|
|
25
|
+
raise SystemExit(run_setup())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def search(
|
|
30
|
+
experiment: Optional[str] = typer.Option(
|
|
31
|
+
None, "-e", "--experiment", help="Experiment name"
|
|
32
|
+
),
|
|
33
|
+
limit: int = typer.Option(10, "-l", "--limit", help="Max traces to return"),
|
|
34
|
+
hours: Optional[int] = typer.Option(None, help="Search last N hours"),
|
|
35
|
+
since: Optional[str] = typer.Option(
|
|
36
|
+
None, help="Search since datetime (ISO format)"
|
|
37
|
+
),
|
|
38
|
+
format: str = typer.Option(
|
|
39
|
+
"summary", "-f", "--format", help="Output: summary|json|context|tools"
|
|
40
|
+
),
|
|
41
|
+
trace_id: Optional[str] = typer.Option(
|
|
42
|
+
None, "--trace-id", help="Get specific trace by ID"
|
|
43
|
+
),
|
|
44
|
+
):
|
|
45
|
+
"""Search and retrieve traces."""
|
|
46
|
+
load_settings() # Load .claude/settings.json env vars
|
|
47
|
+
client = TracingClient()
|
|
48
|
+
|
|
49
|
+
if trace_id:
|
|
50
|
+
trace = client.get_trace(trace_id)
|
|
51
|
+
if not trace:
|
|
52
|
+
typer.echo(f"Trace not found: {trace_id}", err=True)
|
|
53
|
+
raise typer.Exit(1)
|
|
54
|
+
traces = [trace]
|
|
55
|
+
elif hours or since:
|
|
56
|
+
since_dt = datetime.fromisoformat(since) if since else None
|
|
57
|
+
traces = client.search_traces_by_time(
|
|
58
|
+
experiment_name=experiment, hours=hours, since=since_dt, max_results=limit
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
traces = client.search_traces(experiment_name=experiment, max_results=limit)
|
|
62
|
+
|
|
63
|
+
output = {
|
|
64
|
+
"json": format_traces_json,
|
|
65
|
+
"context": format_for_context,
|
|
66
|
+
"tools": format_tool_usage,
|
|
67
|
+
}.get(format, format_traces_summary)(traces)
|
|
68
|
+
|
|
69
|
+
typer.echo(output)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command("list")
|
|
73
|
+
def list_experiments():
|
|
74
|
+
"""List available experiments."""
|
|
75
|
+
load_settings() # Load .claude/settings.json env vars
|
|
76
|
+
client = TracingClient()
|
|
77
|
+
experiments = client.list_experiments()
|
|
78
|
+
|
|
79
|
+
if not experiments:
|
|
80
|
+
typer.echo("No experiments found.")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
typer.echo("Available experiments:")
|
|
84
|
+
for exp in experiments:
|
|
85
|
+
typer.echo(f" [{exp['id']}] {exp['name']}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
app()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""MLFlow tracing client wrapper."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
import mlflow
|
|
6
|
+
from mlflow import MlflowClient
|
|
7
|
+
from mlflow.entities import Trace
|
|
8
|
+
|
|
9
|
+
from .models import SpanInfo, TraceData, TraceInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TracingClient:
|
|
13
|
+
"""Client for retrieving MLFlow traces from Claude Code sessions."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, tracking_uri: str | None = None):
|
|
16
|
+
"""Initialize the tracing client.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
tracking_uri: MLFlow tracking URI. If None, uses MLFLOW_TRACKING_URI env var.
|
|
20
|
+
"""
|
|
21
|
+
if tracking_uri:
|
|
22
|
+
mlflow.set_tracking_uri(tracking_uri)
|
|
23
|
+
self._client = MlflowClient()
|
|
24
|
+
|
|
25
|
+
def list_experiments(self) -> list[dict]:
|
|
26
|
+
"""List all available experiments.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
List of experiment info dicts with id, name, and artifact_location.
|
|
30
|
+
"""
|
|
31
|
+
experiments = self._client.search_experiments()
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
"id": exp.experiment_id,
|
|
35
|
+
"name": exp.name,
|
|
36
|
+
"artifact_location": exp.artifact_location,
|
|
37
|
+
}
|
|
38
|
+
for exp in experiments
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
def get_experiment_id(self, experiment_name: str) -> str | None:
|
|
42
|
+
"""Get experiment ID by name.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
experiment_name: Name of the experiment.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Experiment ID or None if not found.
|
|
49
|
+
"""
|
|
50
|
+
exp = self._client.get_experiment_by_name(experiment_name)
|
|
51
|
+
return exp.experiment_id if exp else None
|
|
52
|
+
|
|
53
|
+
def search_traces(
|
|
54
|
+
self,
|
|
55
|
+
experiment_name: str | None = None,
|
|
56
|
+
experiment_id: str | None = None,
|
|
57
|
+
filter_string: str | None = None,
|
|
58
|
+
max_results: int = 100,
|
|
59
|
+
order_by: list[str] | None = None,
|
|
60
|
+
) -> list[TraceData]:
|
|
61
|
+
"""Search for traces in an experiment.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
experiment_name: Name of the experiment to search.
|
|
65
|
+
experiment_id: ID of the experiment (alternative to name).
|
|
66
|
+
filter_string: MLFlow filter string for traces.
|
|
67
|
+
max_results: Maximum number of traces to return.
|
|
68
|
+
order_by: List of fields to order by (e.g., ["timestamp DESC"]).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of TraceData objects.
|
|
72
|
+
"""
|
|
73
|
+
if experiment_name and not experiment_id:
|
|
74
|
+
experiment_id = self.get_experiment_id(experiment_name)
|
|
75
|
+
if not experiment_id:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
locations = [experiment_id] if experiment_id else None
|
|
79
|
+
|
|
80
|
+
traces = mlflow.search_traces(
|
|
81
|
+
locations=locations,
|
|
82
|
+
filter_string=filter_string,
|
|
83
|
+
max_results=max_results,
|
|
84
|
+
order_by=order_by or ["timestamp DESC"],
|
|
85
|
+
return_type="list",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return [self._convert_trace(t) for t in traces]
|
|
89
|
+
|
|
90
|
+
def search_traces_by_time(
|
|
91
|
+
self,
|
|
92
|
+
experiment_name: str | None = None,
|
|
93
|
+
hours: int | None = None,
|
|
94
|
+
since: datetime | None = None,
|
|
95
|
+
until: datetime | None = None,
|
|
96
|
+
max_results: int = 100,
|
|
97
|
+
) -> list[TraceData]:
|
|
98
|
+
"""Search for traces within a time range.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
experiment_name: Name of the experiment.
|
|
102
|
+
hours: Number of hours back from now to search.
|
|
103
|
+
since: Start datetime for the search range.
|
|
104
|
+
until: End datetime for the search range.
|
|
105
|
+
max_results: Maximum number of traces to return.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of TraceData objects within the time range.
|
|
109
|
+
"""
|
|
110
|
+
if hours:
|
|
111
|
+
since = datetime.now() - timedelta(hours=hours)
|
|
112
|
+
|
|
113
|
+
filter_parts = []
|
|
114
|
+
if since:
|
|
115
|
+
# MLFlow uses milliseconds for timestamp filters
|
|
116
|
+
since_ms = int(since.timestamp() * 1000)
|
|
117
|
+
filter_parts.append(f"timestamp >= {since_ms}")
|
|
118
|
+
if until:
|
|
119
|
+
until_ms = int(until.timestamp() * 1000)
|
|
120
|
+
filter_parts.append(f"timestamp <= {until_ms}")
|
|
121
|
+
|
|
122
|
+
filter_string = " AND ".join(filter_parts) if filter_parts else None
|
|
123
|
+
|
|
124
|
+
return self.search_traces(
|
|
125
|
+
experiment_name=experiment_name,
|
|
126
|
+
filter_string=filter_string,
|
|
127
|
+
max_results=max_results,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def get_trace(self, trace_id: str) -> TraceData | None:
|
|
131
|
+
"""Get a single trace by ID.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
trace_id: The trace ID to retrieve.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
TraceData object or None if not found.
|
|
138
|
+
"""
|
|
139
|
+
trace = self._client.get_trace(trace_id)
|
|
140
|
+
return self._convert_trace(trace) if trace else None
|
|
141
|
+
|
|
142
|
+
def _convert_trace(self, trace: Trace) -> TraceData:
|
|
143
|
+
"""Convert MLFlow Trace to TraceData model.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
trace: MLFlow Trace entity.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
TraceData model instance.
|
|
150
|
+
"""
|
|
151
|
+
info = TraceInfo(
|
|
152
|
+
trace_id=trace.info.request_id,
|
|
153
|
+
request_id=trace.info.request_id,
|
|
154
|
+
experiment_id=trace.info.experiment_id,
|
|
155
|
+
timestamp=datetime.fromtimestamp(trace.info.timestamp_ms / 1000)
|
|
156
|
+
if trace.info.timestamp_ms
|
|
157
|
+
else None,
|
|
158
|
+
execution_time_ms=trace.info.execution_time_ms,
|
|
159
|
+
status=str(trace.info.status) if trace.info.status else None,
|
|
160
|
+
tags=dict(trace.info.tags) if trace.info.tags else {},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
spans = []
|
|
164
|
+
if trace.data and trace.data.spans:
|
|
165
|
+
for span in trace.data.spans:
|
|
166
|
+
spans.append(
|
|
167
|
+
SpanInfo(
|
|
168
|
+
span_id=span.span_id,
|
|
169
|
+
name=span.name,
|
|
170
|
+
parent_id=span.parent_id,
|
|
171
|
+
start_time=datetime.fromtimestamp(span.start_time_ns / 1e9)
|
|
172
|
+
if span.start_time_ns
|
|
173
|
+
else None,
|
|
174
|
+
end_time=datetime.fromtimestamp(span.end_time_ns / 1e9)
|
|
175
|
+
if span.end_time_ns
|
|
176
|
+
else None,
|
|
177
|
+
status=str(span.status) if span.status else None,
|
|
178
|
+
inputs=dict(span.inputs) if span.inputs else {},
|
|
179
|
+
outputs=dict(span.outputs) if span.outputs else {},
|
|
180
|
+
attributes=dict(span.attributes) if span.attributes else {},
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return TraceData(info=info, spans=spans)
|