agent-session-otel 0.2.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.
Files changed (34) hide show
  1. agent_session_otel-0.2.0/.github/workflows/ci.yml +118 -0
  2. agent_session_otel-0.2.0/.github/workflows/publish.yml +34 -0
  3. agent_session_otel-0.2.0/.gitignore +14 -0
  4. agent_session_otel-0.2.0/CHANGELOG.md +48 -0
  5. agent_session_otel-0.2.0/CONTRIBUTING.md +50 -0
  6. agent_session_otel-0.2.0/LICENSE +21 -0
  7. agent_session_otel-0.2.0/PKG-INFO +332 -0
  8. agent_session_otel-0.2.0/README.md +290 -0
  9. agent_session_otel-0.2.0/SECURITY.md +32 -0
  10. agent_session_otel-0.2.0/pyproject.toml +60 -0
  11. agent_session_otel-0.2.0/src/agent_session_otel/__init__.py +10 -0
  12. agent_session_otel-0.2.0/src/agent_session_otel/adapters/__init__.py +12 -0
  13. agent_session_otel-0.2.0/src/agent_session_otel/adapters/base.py +128 -0
  14. agent_session_otel-0.2.0/src/agent_session_otel/adapters/claude_code.py +248 -0
  15. agent_session_otel-0.2.0/src/agent_session_otel/adapters/codex.py +417 -0
  16. agent_session_otel-0.2.0/src/agent_session_otel/cli.py +272 -0
  17. agent_session_otel-0.2.0/src/agent_session_otel/discovery.py +94 -0
  18. agent_session_otel-0.2.0/src/agent_session_otel/doctor.py +160 -0
  19. agent_session_otel-0.2.0/src/agent_session_otel/otel_export.py +594 -0
  20. agent_session_otel-0.2.0/src/agent_session_otel/redaction.py +268 -0
  21. agent_session_otel-0.2.0/src/agent_session_otel/schema.py +118 -0
  22. agent_session_otel-0.2.0/tests/__init__.py +0 -0
  23. agent_session_otel-0.2.0/tests/conftest.py +19 -0
  24. agent_session_otel-0.2.0/tests/fixtures/claude_code/cc-session-001.jsonl +8 -0
  25. agent_session_otel-0.2.0/tests/fixtures/codex/rollout-2026-08-01T11-00-00-abcdef.jsonl +10 -0
  26. agent_session_otel-0.2.0/tests/test_adapters_claude_code.py +84 -0
  27. agent_session_otel-0.2.0/tests/test_adapters_codex.py +82 -0
  28. agent_session_otel-0.2.0/tests/test_adapters_codex_response_items.py +117 -0
  29. agent_session_otel-0.2.0/tests/test_cli.py +115 -0
  30. agent_session_otel-0.2.0/tests/test_discovery.py +27 -0
  31. agent_session_otel-0.2.0/tests/test_doctor.py +73 -0
  32. agent_session_otel-0.2.0/tests/test_export.py +261 -0
  33. agent_session_otel-0.2.0/tests/test_hostile_inputs.py +178 -0
  34. agent_session_otel-0.2.0/tests/test_redaction.py +69 -0
@@ -0,0 +1,118 @@
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
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
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 package with dev extras
25
+ run: python -m pip install -e ".[dev]"
26
+
27
+ - name: Lint
28
+ run: ruff check src tests
29
+
30
+ - name: Test
31
+ run: pytest --cov=agent_session_otel --cov-report=term-missing
32
+
33
+ - name: CLI smoke test
34
+ run: |
35
+ agent-session-otel doctor
36
+ agent-session-otel inspect --claude-code-root tests/fixtures/claude_code --codex-root tests/fixtures/codex
37
+ agent-session-otel export --format json --output trace.json --claude-code-root tests/fixtures/claude_code --codex-root tests/fixtures/codex
38
+ python -c "import json; json.load(open('trace.json'))"
39
+
40
+ # A minimal representative check on macOS/Windows -- one Python version
41
+ # each, not the full matrix, so CI stays fast while still catching
42
+ # platform-specific path/permissions bugs that Linux-only CI would miss.
43
+ cross-platform:
44
+ strategy:
45
+ fail-fast: false
46
+ matrix:
47
+ os: [windows-latest, macos-latest]
48
+ runs-on: ${{ matrix.os }}
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+ - uses: actions/setup-python@v5
52
+ with:
53
+ python-version: "3.12"
54
+ - name: Install package with dev extras
55
+ run: python -m pip install -e ".[dev]"
56
+ - name: Test
57
+ run: pytest
58
+ - name: CLI smoke test
59
+ run: |
60
+ agent-session-otel doctor
61
+ agent-session-otel inspect --claude-code-root tests/fixtures/claude_code --codex-root tests/fixtures/codex
62
+ agent-session-otel export --format json --output trace.json --claude-code-root tests/fixtures/claude_code --codex-root tests/fixtures/codex
63
+ python -c "import json; json.load(open('trace.json'))"
64
+
65
+ # Base install (no extras) must work with zero third-party dependencies
66
+ # for `inspect`/`doctor`, and fail cleanly (not with a traceback) for
67
+ # `export` -- this is a packaging invariant, not just a unit-test one.
68
+ base-install-no-extras:
69
+ runs-on: ubuntu-latest
70
+ steps:
71
+ - uses: actions/checkout@v4
72
+ - uses: actions/setup-python@v5
73
+ with:
74
+ python-version: "3.12"
75
+ - run: python -m pip install .
76
+ - run: agent-session-otel --help
77
+ - run: agent-session-otel doctor
78
+ - run: agent-session-otel inspect --claude-code-root tests/fixtures/claude_code --codex-root tests/fixtures/codex
79
+ - name: export without the otel extra fails cleanly, not with a traceback
80
+ run: |
81
+ set +e
82
+ agent-session-otel export --format json --output trace.json \
83
+ --claude-code-root tests/fixtures/claude_code --codex-root tests/fixtures/codex 2> err.txt
84
+ code=$?
85
+ cat err.txt
86
+ if [ "$code" -ne 1 ] || grep -q "Traceback" err.txt; then
87
+ echo "expected a clean exit-1 error, got exit $code with a traceback"
88
+ exit 1
89
+ fi
90
+
91
+ build:
92
+ runs-on: ubuntu-latest
93
+ needs: test
94
+ steps:
95
+ - uses: actions/checkout@v4
96
+ - uses: actions/setup-python@v5
97
+ with:
98
+ python-version: "3.12"
99
+ - name: Build sdist and wheel
100
+ run: |
101
+ python -m pip install build
102
+ python -m build
103
+ - name: Check package metadata
104
+ run: |
105
+ python -m pip install twine
106
+ twine check dist/*
107
+ - name: Install the built wheel into a clean venv and smoke-test it
108
+ run: |
109
+ python -m venv /tmp/wheel-venv
110
+ /tmp/wheel-venv/bin/pip install "$(ls dist/*.whl)[otel]"
111
+ /tmp/wheel-venv/bin/agent-session-otel --help
112
+ /tmp/wheel-venv/bin/agent-session-otel export --format json --output /tmp/wheel-trace.json \
113
+ --claude-code-root tests/fixtures/claude_code --codex-root tests/fixtures/codex
114
+ python -c "import json; json.load(open('/tmp/wheel-trace.json'))"
115
+ - uses: actions/upload-artifact@v4
116
+ with:
117
+ name: dist
118
+ path: dist/
@@ -0,0 +1,34 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with:
14
+ python-version: "3.12"
15
+ - run: python -m pip install build
16
+ - run: python -m build
17
+ - uses: actions/upload-artifact@v4
18
+ with:
19
+ name: dist
20
+ path: dist/
21
+
22
+ publish:
23
+ runs-on: ubuntu-latest
24
+ needs: build
25
+ environment: pypi
26
+ permissions:
27
+ id-token: write # trusted publishing (OIDC), no API token needed
28
+ steps:
29
+ - uses: actions/download-artifact@v4
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+ - name: Publish to PyPI
34
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ *.egg
14
+ .mypy_cache/
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ Production-hardening pass ahead of the first public release.
6
+
7
+ - **Privacy:** local file paths (`cwd`, session source file) are now
8
+ redacted by default instead of passed through; path-shaped dict keys
9
+ (e.g. Claude Code's file-backup tracking) are redacted, not just
10
+ values; a secret-pattern scrubber (API keys, bearer tokens, private
11
+ key blocks, URL-embedded credentials, ...) now runs unconditionally,
12
+ including under `--include-content`.
13
+ - **Idempotency:** trace/span ids are now derived deterministically from
14
+ `(vendor, session id, event position)` instead of randomly generated,
15
+ so re-running `export` against unchanged sessions reproduces the same
16
+ ids, and exact duplicate events within one run are detected and
17
+ exported once (`asot.duplicate_events_skipped`).
18
+ - **OTel semantics:** migrated from the retired `gen_ai.system` attribute
19
+ to `gen_ai.provider.name`; added `gen_ai.agent.name`,
20
+ `gen_ai.operation.name`, and the opt-in `gen_ai.input.messages`/
21
+ `gen_ai.output.messages` shape; renamed span types that aren't actually
22
+ part of the GenAI spec (`gen_ai.usage` → `asot.usage_snapshot`,
23
+ `gen_ai.error` → `asot.error_event`, `gen_ai.session_event` →
24
+ `asot.session_event`) so only real spec attributes/spans use `gen_ai.*`;
25
+ matched tool call/result pairs now merge into one `execute_tool` span
26
+ with a real duration instead of two disconnected zero-duration spans.
27
+ - **Resilience:** session files are streamed line-by-line instead of
28
+ loaded into memory (tested against a 200k-event/41MB synthetic session);
29
+ a file that can't be opened at all becomes one `unknown` event instead
30
+ of crashing the whole scan; discovery no longer assumes `$HOME`
31
+ resolves.
32
+ - **CLI:** unexpected errors (bad output path, malformed OTLP endpoint,
33
+ Ctrl+C) now produce a one-line message and a sane exit code instead of
34
+ a raw Python traceback; `--verbose` shows the full traceback when you
35
+ want it.
36
+ - **Packaging:** `opentelemetry-api`/`opentelemetry-sdk` moved from hard
37
+ dependencies to the new `otel` extra -- `inspect`/`doctor` now install
38
+ with zero third-party dependencies. `export` needs `[otel]`;
39
+ `export --format otlp` needs `[otlp]`.
40
+ - **Schema:** added an explicit `SCHEMA_VERSION` and documented the
41
+ canonical-schema invariants (vendor concepts never become new
42
+ top-level fields).
43
+
44
+ ## 0.1.0
45
+
46
+ Initial release: Claude Code and Codex CLI adapters, canonical event
47
+ schema, redaction, local-JSON and OTLP export, `inspect`/`export`/`doctor`
48
+ CLI.
@@ -0,0 +1,50 @@
1
+ # Contributing
2
+
3
+ Bug reports and PRs are welcome.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/ryan-wolbeck/agent-session-otel.git
9
+ cd agent-session-otel
10
+ pip install -e ".[dev]"
11
+ pytest
12
+ ruff check src tests
13
+ ```
14
+
15
+ ## Adding or updating adapter behavior
16
+
17
+ Claude Code and Codex's JSONL formats are undocumented and change
18
+ between releases. If you're adding support for a new vendor event shape:
19
+
20
+ 1. Add a fixture line to `tests/fixtures/claude_code/` or
21
+ `tests/fixtures/codex/` (or a synthetic record in
22
+ `tests/test_hostile_inputs.py` if it's an edge case rather than a
23
+ realistic full session).
24
+ 2. Map it to one of the five canonical event types in
25
+ `agent_session_otel/schema.py` (`session`/`turn`/`tool`/`usage`/`error`)
26
+ -- or, if it genuinely doesn't fit, leave it as `unknown`. Don't add a
27
+ new top-level schema field to accommodate a vendor-specific concept;
28
+ see the invariants documented at the top of `schema.py`.
29
+ 3. Never put free-text content in `extra` -- it bypasses redaction.
30
+ Content belongs in `content`/`tool_input`/`tool_output`/`error_message`/`raw`.
31
+ 4. If it's genuinely new information, run it past `otel_export.py`'s
32
+ module docstring before inventing a `gen_ai.*`-looking attribute name.
33
+ Check the [GenAI semantic conventions registry](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/model/gen-ai/registry.yaml)
34
+ first; if it's not there, use the `asot.*` namespace instead.
35
+
36
+ ## Privacy-sensitive changes
37
+
38
+ Anything touching redaction (`redaction.py`), what gets attached to
39
+ exported spans (`otel_export.py`), or what a normalized event exposes
40
+ (`schema.py`) should come with a test proving the sensitive value does
41
+ *not* appear in default (non-`--include-content`) output. See
42
+ `tests/test_redaction.py` and the redaction-specific tests in
43
+ `tests/test_export.py` for the pattern.
44
+
45
+ ## Running the CLI against your own real sessions
46
+
47
+ `agent-session-otel doctor` and `agent-session-otel inspect` are safe to
48
+ run against your real `~/.claude/projects` / `~/.codex/sessions` --
49
+ discovery is read-only and inspection is redacted by default. Don't paste
50
+ `--include-content` output into an issue or PR.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan Wolbeck
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,332 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-session-otel
3
+ Version: 0.2.0
4
+ Summary: Discover local Claude Code / Codex session logs and export them as OpenTelemetry GenAI-compatible traces.
5
+ Project-URL: Homepage, https://github.com/ryan-wolbeck/agent-session-otel
6
+ Project-URL: Issues, https://github.com/ryan-wolbeck/agent-session-otel/issues
7
+ Author: Ryan Wolbeck
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: claude-code,codex,genai,observability,opentelemetry,otel,tracing
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
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 :: Debuggers
21
+ Classifier: Topic :: System :: Logging
22
+ Classifier: Topic :: System :: Monitoring
23
+ Requires-Python: >=3.9
24
+ Provides-Extra: dev
25
+ Requires-Dist: opentelemetry-api>=1.24; extra == 'dev'
26
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.24; extra == 'dev'
27
+ Requires-Dist: opentelemetry-sdk>=1.24; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=4.1; extra == 'dev'
29
+ Requires-Dist: pytest>=7.4; extra == 'dev'
30
+ Requires-Dist: ruff>=0.5; extra == 'dev'
31
+ Provides-Extra: otel
32
+ Requires-Dist: opentelemetry-api>=1.24; extra == 'otel'
33
+ Requires-Dist: opentelemetry-sdk>=1.24; extra == 'otel'
34
+ Provides-Extra: otlp
35
+ Requires-Dist: opentelemetry-api>=1.24; extra == 'otlp'
36
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.24; extra == 'otlp'
37
+ Requires-Dist: opentelemetry-sdk>=1.24; extra == 'otlp'
38
+ Provides-Extra: test
39
+ Requires-Dist: pytest-cov>=4.1; extra == 'test'
40
+ Requires-Dist: pytest>=7.4; extra == 'test'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # agent-session-otel
44
+
45
+ Turn the session logs [Claude Code](https://claude.com/claude-code) and
46
+ [Codex CLI](https://github.com/openai/codex) already write to your local
47
+ disk into [OpenTelemetry](https://opentelemetry.io/) traces, using the
48
+ (still-evolving) [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai).
49
+
50
+ ```
51
+ Claude Code session logs ──→ ClaudeCodeAdapter ──┐
52
+ ├──→ canonical events ──→ OTel spans ──→ JSON file / OTLP
53
+ Codex CLI session logs ──→ CodexAdapter ──┘
54
+ ```
55
+
56
+ Both vendors' JSONL formats are undocumented, unstable, and evolve
57
+ between releases. This tool treats them as exactly that: two
58
+ disposable *adapters* that normalize into one small, stable, vendor-neutral
59
+ schema (`session` / `turn` / `tool` / `usage` / `error`, plus an `unknown`
60
+ catch-all). Nothing outside the two adapter modules knows Claude Code or
61
+ Codex's field names. When a vendor changes its format -- which will
62
+ happen -- unrecognized events are preserved (not dropped), and adding a
63
+ third vendor doesn't touch the canonical schema or the OTel export code.
64
+
65
+ ## Why not native telemetry?
66
+
67
+ Neither tool exports OpenTelemetry today. Their session logs are replay
68
+ transcripts for their own resume/UI features, not telemetry -- different
69
+ shape, different guarantees, and no cross-vendor consistency. This tool
70
+ reconstructs *after the fact* what a trace of that session would have
71
+ looked like, so you can point your existing observability stack (or a
72
+ local JSON file, no stack required) at your actual coding-agent usage.
73
+ It does not run a web UI, does not run a database, and does not talk to
74
+ any vendor API -- it reads files already on disk and writes OTel data.
75
+
76
+ ## What leaves your machine?
77
+
78
+ **By default: no prompt text, no file contents, no tool arguments/output,
79
+ no local file paths.** Every content-bearing field is replaced with a
80
+ `<redacted:len=N:sha256=...>` placeholder, and local paths (which for
81
+ Claude Code literally encode your project's absolute path into a
82
+ directory name) are collapsed to just a basename. Structural metadata --
83
+ event types, timestamps, token counts, tool *names* (not arguments),
84
+ model names -- passes through, since that's what makes the trace useful
85
+ without being sensitive.
86
+
87
+ Passing `--include-content` opts into carrying real prompt/tool/file
88
+ content through end to end -- do this only when exporting to a
89
+ destination you trust with that content. Even then, a secret-pattern
90
+ scrubber (API keys, bearer tokens, private key blocks, credentials
91
+ embedded in URLs, ...) still runs; `--include-content` opts into your
92
+ own prompts and file content, not into leaking a credential that happens
93
+ to appear in them.
94
+
95
+ See [Privacy defaults](#privacy-defaults) below for the full detail.
96
+
97
+ ## Install
98
+
99
+ ```bash
100
+ # `inspect` and `doctor` work with zero third-party dependencies.
101
+ pip install agent-session-otel
102
+
103
+ # `export` (either format) needs the OTel SDK:
104
+ pip install "agent-session-otel[otel]"
105
+
106
+ # `export --format otlp` additionally needs the OTLP/HTTP exporter:
107
+ pip install "agent-session-otel[otlp]"
108
+ ```
109
+
110
+ ## Quick start
111
+
112
+ ```bash
113
+ # Sanity-check your environment and see what sessions are discoverable.
114
+ agent-session-otel doctor
115
+
116
+ # Summarize local sessions (Claude Code + Codex, redacted).
117
+ agent-session-otel inspect
118
+ ```
119
+ ```
120
+ claude_code cc-a1b2c3d4
121
+ file: <redacted-path>/cc-a1b2c3d4.jsonl
122
+ time range: 2026-08-01T10:00:00.000Z -> 2026-08-01T10:14:22.000Z
123
+ events: session=2, turn=11, tool=6, usage=6, unknown=3
124
+ tokens: input=18300 output=2140
125
+ ```
126
+ ```bash
127
+ # Dump normalized events as JSON lines, content still redacted.
128
+ agent-session-otel inspect --json --vendor claude-code
129
+
130
+ # Export everything found as a local OTel JSON trace document.
131
+ agent-session-otel export --format json --output trace.json
132
+
133
+ # Export to a running OTel collector, including real content (opt-in).
134
+ agent-session-otel export --format otlp \
135
+ --endpoint http://localhost:4318/v1/traces \
136
+ --include-content
137
+ ```
138
+
139
+ ## Commands
140
+
141
+ ### `agent-session-otel inspect`
142
+
143
+ Discovers session files and prints a human-readable summary per session
144
+ (event counts, token totals, time range). `--json` prints one normalized
145
+ event per line instead (the canonical schema -- see below).
146
+
147
+ ### `agent-session-otel export`
148
+
149
+ Normalizes discovered sessions and exports them as an OpenTelemetry
150
+ trace: one root `invoke_agent <agent>` span per session, with one child
151
+ span per normalized event -- `chat` for turns, `execute_tool` for tool
152
+ calls (merged with their matching result into one span, when both are
153
+ present, so span duration reflects the tool's actual runtime), and
154
+ `asot.*`-namespaced spans for usage snapshots, diagnostic/session events,
155
+ errors, and anything preserved-but-unrecognized.
156
+
157
+ `--format json` needs no collector -- it writes a self-contained JSON
158
+ document of the spans. `--format otlp` requires the `otlp` extra and a
159
+ reachable OTLP/HTTP traces endpoint.
160
+
161
+ Re-running `export` against the same, unmodified session files produces
162
+ the same trace/span ids every time (derived from the vendor's session id
163
+ and each event's position in the file, not randomly generated), so a
164
+ backend that dedupes on (trace_id, span_id) recognizes a repeated import
165
+ instead of double-counting it. See [Idempotency](#idempotency-re-running-export).
166
+
167
+ ### `agent-session-otel doctor`
168
+
169
+ Checks the Python version, whether the OTel SDK / OTLP exporter extras
170
+ are installed, and does an end-to-end discovery + parse smoke test
171
+ against your real session directories. Exits non-zero only on fatal
172
+ problems -- a missing session directory, or the OTel extras not being
173
+ installed, are warnings, not errors, since `inspect`/`doctor` don't need
174
+ them.
175
+
176
+ ## Options common to `inspect` and `export`
177
+
178
+ | Flag | Description |
179
+ | --- | --- |
180
+ | `--vendor {claude-code,codex,all}` | Limit to one vendor. Default: `all`. |
181
+ | `--claude-code-root PATH` | Override the Claude Code session root (repeatable). Default: `~/.claude/projects`. |
182
+ | `--codex-root PATH` | Override the Codex session root (repeatable). Default: `~/.codex/sessions`. |
183
+ | `--session-id ID` | Limit to session(s) whose filename matches `ID` (repeatable). |
184
+ | `--include-content` | Opt-in: carry real prompt/tool/error text through instead of redacting it. |
185
+ | `--verbose` | Show a full traceback (instead of a one-line message) if something unexpected fails. |
186
+
187
+ Session roots can also be set via `AGENT_SESSION_OTEL_CLAUDE_CODE_HOME` and
188
+ `AGENT_SESSION_OTEL_CODEX_HOME`. Neither Claude Code, Codex, nor the
189
+ directories they write to are ever installed or required -- `doctor` and
190
+ `inspect` simply report zero sessions found for whichever vendor isn't
191
+ present on your machine, which is not an error.
192
+
193
+ ## Canonical schema
194
+
195
+ Every adapter normalizes into `agent_session_otel.schema.NormalizedEvent`:
196
+ one of `session` / `turn` / `tool` / `usage` / `error` / `unknown`, plus
197
+ role, model, content, tool name/id/input/output, token usage, an
198
+ `extra` dict for small structural vendor-specific tags, and `raw` holding
199
+ the original vendor record for lossless preservation. `SCHEMA_VERSION`
200
+ (currently `1`, present on every exported/inspected event) bumps whenever
201
+ a field's meaning changes, so downstream consumers can detect a schema
202
+ they don't understand.
203
+
204
+ **Invariant:** vendor-specific concepts never become new top-level
205
+ schema fields. If a Claude Code or Codex event doesn't map onto one of
206
+ the five categories, it becomes `unknown` with the original record
207
+ preserved -- it is not dropped, and the schema does not grow a
208
+ Claude-Code-shaped or Codex-shaped field to accommodate it.
209
+
210
+ ## OpenTelemetry mapping
211
+
212
+ Span and attribute names follow the
213
+ [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai)
214
+ wherever a convention genuinely exists for the concept being represented
215
+ (`gen_ai.provider.name`, `gen_ai.agent.name`, `gen_ai.operation.name`,
216
+ `gen_ai.conversation.id`, `gen_ai.usage.*`, `gen_ai.tool.*`,
217
+ `gen_ai.input.messages` / `gen_ai.output.messages`). **The entire GenAI
218
+ semconv is `development`-stability as of this writing** -- expect
219
+ upstream attribute renames (this project already migrated once, from the
220
+ now-retired `gen_ai.system` to `gen_ai.provider.name`).
221
+
222
+ Anything this tool needs that the spec doesn't define is namespaced
223
+ under `asot.*` rather than given an official-looking `gen_ai.*` name:
224
+ `asot.usage_snapshot` and `asot.error_event` (standalone spans for
225
+ events that don't cleanly attach to one chat/tool span), `asot.session_event`
226
+ (session-level metadata), `asot.unknown_event` (preserved-but-unmodeled
227
+ vendor events, full redacted payload in `asot.raw`), and
228
+ `asot.duplicate_events_skipped` (see Idempotency).
229
+
230
+ **Reconstructed vs. live telemetry:** these are historical traces built
231
+ from a static log file, not live instrumentation. Span kind is always
232
+ `INTERNAL`. A tool span's duration is the gap between its call and result
233
+ *as logged*, not a live-measured duration. Timestamps the vendor didn't
234
+ record are synthesized (nudged forward by 1μs from the prior event) purely
235
+ so spans stay orderable -- they are not real wall-clock times. None of
236
+ this is hidden: it's documented in `otel_export.py`'s module docstring,
237
+ which is worth reading before you alert on these traces as if they were
238
+ live data.
239
+
240
+ ## Idempotency (re-running `export`)
241
+
242
+ Trace and span ids are derived deterministically from `(vendor, session
243
+ id, event position in file)` -- not randomly generated. Re-running
244
+ `export` against unchanged session files reliably reproduces the same
245
+ ids, and within a single run, an exact repeat of the same event (e.g. the
246
+ same file reachable from two overlapping `--claude-code-root` values)
247
+ is detected and exported once, with the count of skipped repeats on the
248
+ session's root span (`asot.duplicate_events_skipped`).
249
+
250
+ This is a best-effort identity scheme, not a cryptographic guarantee: a
251
+ session id collision across genuinely different content, or a file
252
+ edited/reordered in place rather than purely appended to, would not be
253
+ caught. It is keyed on the vendor's own session id plus each event's
254
+ line position, not on the local absolute file path (which isn't part of
255
+ a session's identity, and which redaction hides by default anyway).
256
+
257
+ ## Privacy defaults
258
+
259
+ By default, every leaf string value that could contain user or model
260
+ content -- message text, tool arguments/output, error messages, local
261
+ file paths (including dict *keys* that are themselves paths, e.g. Claude
262
+ Code's file-backup tracking), and the equivalent fields inside preserved
263
+ raw vendor payloads -- is replaced with `<redacted:len=N:sha256=...>` (or,
264
+ for paths, collapsed to `<redacted-path>/basename`). Structural fields
265
+ (ids, timestamps, roles, event types, tool *names*, token counts, model
266
+ names) are never redacted.
267
+
268
+ `--include-content` disables that wholesale redaction and carries real
269
+ content through -- but a separate secret-pattern scrubber (AWS keys,
270
+ GitHub/Slack tokens, JWTs, bearer tokens, private key blocks, URL-embedded
271
+ credentials, common `key=`/`token=`/`password=` assignments) always
272
+ runs, opt-in or not, replacing matches with `<redacted-secret:label>`.
273
+ It is a best-effort net, not a guarantee -- it cannot catch every secret
274
+ shape, especially ones split across multiple tokens or non-standard
275
+ formats. Treat `--include-content` output as sensitive regardless.
276
+
277
+ ## Filesystem safety
278
+
279
+ Discovery only ever reads `*.jsonl` files under the configured roots --
280
+ it never writes to, modifies, or deletes anything Claude Code or Codex
281
+ manages. A permission-denied subdirectory, a non-directory root, a
282
+ symlink loop, or an unresolvable `$HOME` are all handled gracefully
283
+ (skipped with a warning, not a crash); a single unreadable or malformed
284
+ *file* becomes one `unknown` event rather than aborting the whole scan.
285
+
286
+ ## Supported versions and forward compatibility
287
+
288
+ There is no official spec for either vendor's session JSONL format, and
289
+ both have changed shape across releases (this project's Codex adapter was
290
+ written against a schema noticeably more complex than what was documented
291
+ in older community write-ups). Every field lookup in both adapters is
292
+ defensive (`.get()` with fallbacks, never an assumed key), and anything
293
+ that doesn't match a known shape -- a new top-level record type, a new
294
+ tool-call variant, a field that's now `null` where it used to be a string
295
+ -- becomes an `unknown` normalized event carrying the original record,
296
+ rather than raising or silently vanishing. On real Claude Code and Codex
297
+ history on the machine this was developed on, a meaningful fraction of
298
+ events fall into `unknown` today; that number going up over time as
299
+ vendors ship changes is expected, not a bug, and `agent-session-otel
300
+ doctor` reports it directly so you can see your own coverage.
301
+
302
+ ## Development
303
+
304
+ ```bash
305
+ git clone https://github.com/ryan-wolbeck/agent-session-otel.git
306
+ cd agent-session-otel
307
+ pip install -e ".[dev]"
308
+ pytest
309
+ ruff check src tests
310
+ ```
311
+
312
+ Fixture-based tests live under `tests/fixtures/`. `tests/test_hostile_inputs.py`
313
+ specifically targets malformed/truncated/empty/oversized/non-UTF-8 input;
314
+ `tests/test_export.py` covers OTel semantics, deterministic ids, and
315
+ redaction; `tests/test_redaction.py` and the secret-scrubbing tests in
316
+ `test_export.py` are the ones to extend first if you're touching privacy
317
+ behavior. See [CONTRIBUTING.md](CONTRIBUTING.md).
318
+
319
+ ## Scope
320
+
321
+ This project is a CLI that reads local JSONL files and writes OTel
322
+ traces. It does not run a web UI, does not run a database, does not
323
+ require authentication, and does not talk to any vendor API. It's meant
324
+ to sit alongside your existing observability stack, not replace it.
325
+
326
+ ## Security
327
+
328
+ See [SECURITY.md](SECURITY.md) for how to report a vulnerability.
329
+
330
+ ## License
331
+
332
+ MIT