Skip to content

Commit 1ac838a

Browse files
committed
Merge branch 'main' into benchmark
2 parents d57e914 + 48a19ca commit 1ac838a

119 files changed

Lines changed: 16928 additions & 2898 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/BUGBOT.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# BUGBOT Notes
2+
3+
## Instrumentation Guidelines
4+
5+
- When adding a new instrumentation, the README must be updated to document the new instrumentation.

.github/workflows/ci.yml

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
version: "latest"
2121

2222
- name: Setup Python
23-
run: uv python install 3.12
23+
run: uv python install 3.9
2424

2525
- name: Install dependencies
2626
run: uv sync --all-extras
@@ -44,7 +44,7 @@ jobs:
4444
version: "latest"
4545

4646
- name: Setup Python
47-
run: uv python install 3.12
47+
run: uv python install 3.9
4848

4949
- name: Install dependencies
5050
run: uv sync --all-extras
@@ -53,8 +53,13 @@ jobs:
5353
run: uv run ty check drift/ tests/
5454

5555
test:
56-
name: Unit Tests
56+
name: Unit Tests (Python ${{ matrix.python-version }})
5757
runs-on: ubuntu-latest
58+
timeout-minutes: 30
59+
strategy:
60+
fail-fast: false
61+
matrix:
62+
python-version: ["3.9", "3.14"]
5863
steps:
5964
- name: Checkout
6065
uses: actions/checkout@v4
@@ -65,13 +70,33 @@ jobs:
6570
version: "latest"
6671

6772
- name: Setup Python
68-
run: uv python install 3.12
73+
run: uv python install ${{ matrix.python-version }}
74+
75+
- name: Cache uv + Python installs + venv
76+
uses: actions/cache@v4
77+
with:
78+
path: |
79+
~/.cache/uv
80+
~/.local/share/uv/python
81+
.venv
82+
key: ${{ runner.os }}-uv-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
6983

7084
- name: Install dependencies
7185
run: uv sync --all-extras
7286

7387
- name: Run unit tests
74-
run: uv run pytest tests/unit/ -v
88+
if: matrix.python-version != '3.9'
89+
run: uv run pytest tests/unit/ -q
90+
91+
- name: Run unit tests with coverage
92+
if: matrix.python-version == '3.9'
93+
run: uv run pytest tests/unit/ -q --cov=drift/core --cov-report=xml
94+
95+
- name: Upload coverage to Coveralls
96+
if: matrix.python-version == '3.9' && github.event_name == 'push' && github.ref == 'refs/heads/main'
97+
uses: coverallsapp/github-action@v2
98+
with:
99+
file: coverage.xml
75100

76101
build:
77102
name: Build Package
@@ -86,7 +111,7 @@ jobs:
86111
version: "latest"
87112

88113
- name: Setup Python
89-
run: uv python install 3.12
114+
run: uv python install 3.9
90115

91116
- name: Install dependencies
92117
run: uv sync --all-extras
@@ -99,4 +124,3 @@ jobs:
99124
uv venv /tmp/test-install
100125
uv pip install dist/*.whl --python /tmp/test-install/bin/python
101126
/tmp/test-install/bin/python -c "import drift; print('Package imported successfully')"
102-

.github/workflows/e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
version: "latest"
5555

5656
- name: Setup Python
57-
run: uv python install 3.12
57+
run: uv python install 3.9
5858

5959
- name: Setup Docker Buildx
6060
uses: docker/setup-buildx-action@v3

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,9 @@ __marimo__/
224224
.DS_Store
225225

226226
# Bug tracking
227-
**/BUG_TRACKING.md
227+
**/BUG_TRACKING.md
228+
229+
# Coverage
230+
coverage.lcov
231+
coverage.xml
232+
.coverage

CONTRIBUTING.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,25 @@ uv run ty check drift/ tests/ # Type check
3131
### Unit Tests
3232

3333
```bash
34-
uv run python -m unittest discover -s tests/unit -v
34+
uv run pytest tests/unit/ -v
35+
36+
# Run with coverage
37+
uv run pytest tests/unit/ -v --cov=drift --cov-report=term-missing
3538

3639
# Run a specific test file
37-
uv run python -m unittest tests.unit.test_json_schema_helper -v
38-
uv run python -m unittest tests.unit.test_adapters -v
40+
uv run pytest tests/unit/test_json_schema_helper.py -v
41+
uv run pytest tests/unit/test_adapters.py -v
42+
43+
# Run a specific test class or function
44+
uv run pytest tests/unit/test_metrics.py::TestMetricsCollector -v
45+
uv run pytest tests/unit/test_metrics.py::TestMetricsCollector::test_record_spans_exported -v
3946
```
4047

4148
### Integration Tests
4249

4350
```bash
4451
# Flask/FastAPI integration tests
45-
timeout 30 uv run python -m unittest discover -s tests/integration -v
52+
timeout 30 uv run pytest tests/integration/ -v
4653
```
4754

4855
### E2E Tests

README.md

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,43 +39,64 @@ For comprehensive guides and API reference, visit our [full documentation](https
3939

4040
## Requirements
4141

42-
- Python 3.12+
42+
- Python 3.9+
4343

4444
Tusk Drift currently supports the following packages and versions:
4545

46-
- **Flask**: `flask>=2.0.0`
47-
- **FastAPI**: `fastapi>=0.68.0`
48-
- **Django**: `django>=3.2.0`
49-
- **Requests**: `requests` (all versions)
50-
- **HTTPX**: `httpx` (all versions)
51-
- **psycopg**: `psycopg>=3.0.0`, `psycopg2>=2.8.0`
52-
- **Redis**: `redis` (all versions)
46+
| Package | Supported Versions |
47+
|---------|-------------------|
48+
| Flask | `>=2.0.0` |
49+
| FastAPI | `>=0.68.0` |
50+
| Django | `>=3.2.0` |
51+
| Requests | all versions |
52+
| HTTPX | all versions |
53+
| aiohttp | all versions |
54+
| urllib3 | all versions |
55+
| grpcio (client-side only) | all versions |
56+
| psycopg | `>=3.1.12` |
57+
| psycopg2 | all versions |
58+
| Redis | `>=4.0.0` |
59+
| Kinde | `>=2.0.1` |
60+
| PyJWT | all versions |
61+
| urllib.request | all versions |
5362

5463
If you're using packages or versions not listed above, please create an issue with the package + version you'd like an instrumentation for.
5564

5665
## Installation
5766

5867
### Step 1: Install the CLI
5968

60-
First, install and configure the Tusk Drift CLI by following our [CLI installation guide](https://github.com/Use-Tusk/tusk-drift-cli?tab=readme-ov-file#install). The CLI helps set up your Tusk configuration file and replays tests.
69+
First, install the Tusk Drift CLI by following our [CLI installation guide](https://github.com/Use-Tusk/tusk-drift-cli?tab=readme-ov-file#install).
6170

62-
The wizard will eventually direct you back here when it's time to set up the SDK.
71+
### Step 2: Set up Tusk Drift
6372

64-
### Step 2: Install the SDK
73+
#### AI-powered setup (recommended)
6574

66-
After completing the CLI wizard, install the SDK:
75+
Use our AI agent to automatically set up Tusk Drift for your service:
6776

6877
```bash
69-
pip install tusk-drift-python-sdk
78+
cd path/to/your/service
79+
export ANTHROPIC_API_KEY=your-api-key
80+
tusk setup
7081
```
7182

72-
### Step 3: Initialize the SDK for your service
83+
The agent will analyze your codebase, install the SDK, instrument it into your application, create configuration files, and test the setup with recording and replay.
7384

74-
Refer to our [initialization guide](docs/initialization.md) to set up the SDK for your service.
85+
#### Manual setup
7586

76-
### Step 4: Run Your First Test
87+
Alternatively, you can set up Tusk Drift manually:
7788

78-
Follow along our [quick start guide](docs/quickstart.md) to record and replay your first test!
89+
1. Install the SDK:
90+
91+
```bash
92+
pip install tusk-drift-python-sdk
93+
```
94+
95+
2. Create configuration: Run `tusk init` to create your `.tusk/config.yaml` config file interactively, or create it manually per the [configuration docs](https://github.com/Use-Tusk/tusk-drift-cli/blob/main/docs/configuration.md).
96+
97+
3. Initialize the SDK: Refer to the [initialization guide](docs/initialization.md) to instrument the SDK in your service.
98+
99+
4. Record and replay: Follow the [quick start guide](docs/quickstart.md) to record and replay your first test!
79100

80101
## Troubleshooting
81102

@@ -87,11 +108,3 @@ Having issues?
87108
## Community
88109

89110
Join our open source community on [Slack](https://join.slack.com/t/tusk-community/shared_invite/zt-3fve1s7ie-NAAUn~UpHsf1m_2tdoGjsQ).
90-
91-
## Contributing
92-
93-
We appreciate feedback and contributions. See [CONTRIBUTING.md](/CONTRIBUTING.md).
94-
95-
## License
96-
97-
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.

docs/initialization.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44

55
Before setting up the SDK, ensure you have:
66

7-
- Completed the [CLI wizard](https://github.com/Use-Tusk/tusk-drift-cli?tab=readme-ov-file#quick-start)
7+
- Python 3.9 or later installed
8+
- Installed the [Tusk Drift CLI](https://github.com/Use-Tusk/tusk-drift-cli?tab=readme-ov-file#install)
89
- Obtained an API key from the [Tusk Drift dashboard](https://usetusk.ai/app/settings/api-keys) (only required if using Tusk Cloud)
9-
- Python 3.12 or later installed
10+
11+
> [!TIP]
12+
> For automated setup, use `tusk setup` which handles SDK installation and initialization for you.
13+
> The steps below are for manual setup.
1014
1115
## Step 1: Install the SDK
1216

drift/core/batch_processor.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def __init__(
6363
self._condition = threading.Condition(self._lock)
6464
self._shutdown_event = threading.Event()
6565
self._export_thread: threading.Thread | None = None
66+
self._thread_loop: asyncio.AbstractEventLoop | None = None
6667
self._started = False
6768
self._dropped_spans = 0
6869

@@ -158,16 +159,23 @@ def add_span(self, span: CleanSpanData) -> bool:
158159

159160
def _export_loop(self) -> None:
160161
"""Background thread that periodically exports spans."""
161-
while not self._shutdown_event.is_set():
162-
# Wait for either: batch size reached, scheduled delay, or shutdown
163-
with self._condition:
164-
# Wait until batch is ready or timeout
165-
self._condition.wait(timeout=self._config.scheduled_delay_seconds)
162+
# Create a single long-lived event loop for this thread
163+
self._thread_loop = asyncio.new_event_loop()
164+
asyncio.set_event_loop(self._thread_loop)
166165

167-
if self._shutdown_event.is_set():
168-
break
166+
try:
167+
while not self._shutdown_event.is_set():
168+
# Wait for either: batch size reached, scheduled delay, or shutdown
169+
with self._condition:
170+
self._condition.wait(timeout=self._config.scheduled_delay_seconds)
169171

170-
self._export_batch()
172+
if self._shutdown_event.is_set():
173+
break
174+
175+
self._export_batch()
176+
finally:
177+
self._thread_loop.close()
178+
self._thread_loop = None
171179

172180
def _export_batch(self) -> None:
173181
"""Export a batch of spans from the queue."""
@@ -188,16 +196,26 @@ def _export_batch(self) -> None:
188196
for adapter in adapters:
189197
start_time = time.monotonic()
190198
try:
191-
# Handle async adapters (create new event loop for this thread)
199+
# Handle async adapters
192200
if asyncio.iscoroutinefunction(adapter.export_spans):
193-
loop = asyncio.new_event_loop()
194-
asyncio.set_event_loop(loop)
195-
try:
196-
loop.run_until_complete(adapter.export_spans(batch))
197-
finally:
198-
loop.close()
201+
# Only reuse the thread's event loop if we're on the export thread.
202+
# Using it from another thread (e.g., force_flush after join timeout)
203+
# would cause RuntimeError since event loops are not thread-safe.
204+
is_export_thread = threading.current_thread() is self._export_thread
205+
can_reuse_loop = (
206+
is_export_thread and self._thread_loop is not None and not self._thread_loop.is_closed()
207+
)
208+
if can_reuse_loop and self._thread_loop is not None:
209+
self._thread_loop.run_until_complete(adapter.export_spans(batch))
210+
else:
211+
loop = asyncio.new_event_loop()
212+
asyncio.set_event_loop(loop)
213+
try:
214+
loop.run_until_complete(adapter.export_spans(batch))
215+
finally:
216+
loop.close()
199217
else:
200-
adapter.export_spans(batch) # type: ignore
218+
adapter.export_spans(batch)
201219

202220
latency_ms = (time.monotonic() - start_time) * 1000
203221
self._metrics.record_spans_exported(len(batch))

0 commit comments

Comments
 (0)