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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/claudetracing.svg)](https://pypi.org/project/claudetracing/)
31
+ [![Python versions](https://img.shields.io/pypi/pyversions/claudetracing.svg)](https://pypi.org/project/claudetracing/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/claudetracing.svg)](https://pypi.org/project/claudetracing/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/claudetracing.svg)](https://pypi.org/project/claudetracing/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)