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.
- agent_session_otel-0.2.0/.github/workflows/ci.yml +118 -0
- agent_session_otel-0.2.0/.github/workflows/publish.yml +34 -0
- agent_session_otel-0.2.0/.gitignore +14 -0
- agent_session_otel-0.2.0/CHANGELOG.md +48 -0
- agent_session_otel-0.2.0/CONTRIBUTING.md +50 -0
- agent_session_otel-0.2.0/LICENSE +21 -0
- agent_session_otel-0.2.0/PKG-INFO +332 -0
- agent_session_otel-0.2.0/README.md +290 -0
- agent_session_otel-0.2.0/SECURITY.md +32 -0
- agent_session_otel-0.2.0/pyproject.toml +60 -0
- agent_session_otel-0.2.0/src/agent_session_otel/__init__.py +10 -0
- agent_session_otel-0.2.0/src/agent_session_otel/adapters/__init__.py +12 -0
- agent_session_otel-0.2.0/src/agent_session_otel/adapters/base.py +128 -0
- agent_session_otel-0.2.0/src/agent_session_otel/adapters/claude_code.py +248 -0
- agent_session_otel-0.2.0/src/agent_session_otel/adapters/codex.py +417 -0
- agent_session_otel-0.2.0/src/agent_session_otel/cli.py +272 -0
- agent_session_otel-0.2.0/src/agent_session_otel/discovery.py +94 -0
- agent_session_otel-0.2.0/src/agent_session_otel/doctor.py +160 -0
- agent_session_otel-0.2.0/src/agent_session_otel/otel_export.py +594 -0
- agent_session_otel-0.2.0/src/agent_session_otel/redaction.py +268 -0
- agent_session_otel-0.2.0/src/agent_session_otel/schema.py +118 -0
- agent_session_otel-0.2.0/tests/__init__.py +0 -0
- agent_session_otel-0.2.0/tests/conftest.py +19 -0
- agent_session_otel-0.2.0/tests/fixtures/claude_code/cc-session-001.jsonl +8 -0
- agent_session_otel-0.2.0/tests/fixtures/codex/rollout-2026-08-01T11-00-00-abcdef.jsonl +10 -0
- agent_session_otel-0.2.0/tests/test_adapters_claude_code.py +84 -0
- agent_session_otel-0.2.0/tests/test_adapters_codex.py +82 -0
- agent_session_otel-0.2.0/tests/test_adapters_codex_response_items.py +117 -0
- agent_session_otel-0.2.0/tests/test_cli.py +115 -0
- agent_session_otel-0.2.0/tests/test_discovery.py +27 -0
- agent_session_otel-0.2.0/tests/test_doctor.py +73 -0
- agent_session_otel-0.2.0/tests/test_export.py +261 -0
- agent_session_otel-0.2.0/tests/test_hostile_inputs.py +178 -0
- 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,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
|