Skip to content

Commit bcb8167

Browse files
merge main
2 parents 3e0cf16 + d2344ff commit bcb8167

28 files changed

Lines changed: 1984 additions & 178 deletions

.github/workflows/e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
fail-fast: false
2323
max-parallel: 6
2424
matrix:
25-
library: [flask, fastapi, django, redis, requests, httpx, psycopg, psycopg2]
25+
library: [flask, fastapi, django, redis, requests, httpx, psycopg, psycopg2, urllib3]
2626
steps:
2727
- name: Checkout
2828
uses: actions/checkout@v4

.tusk/config.yaml

Lines changed: 0 additions & 36 deletions
This file was deleted.

CONTRIBUTING.md

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,6 @@ uv run python -m unittest tests.unit.test_adapters -v
4545
timeout 30 uv run python -m unittest discover -s tests/integration -v
4646
```
4747

48-
### Database Integration Tests
49-
50-
Requires Docker for running test databases.
51-
52-
```bash
53-
# Start test databases
54-
docker compose -f docker-compose.test.yml up -d
55-
56-
# Run database tests
57-
uv run python -m unittest tests.integration.test_database -v
58-
59-
# Stop databases
60-
docker compose -f docker-compose.test.yml down
61-
```
62-
6348
### E2E Tests
6449

6550
E2E tests validate full instrumentation workflows using Docker containers. They record real API interactions and verify replay behavior using the Tusk CLI.

MANIFEST.in

Lines changed: 0 additions & 5 deletions
This file was deleted.

README.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,6 @@ After completing the CLI wizard, install the SDK:
6969
pip install tusk-drift-python-sdk
7070
```
7171

72-
#### With Framework Support
73-
74-
```bash
75-
# With Flask support
76-
pip install tusk-drift-python-sdk[flask]
77-
78-
# With FastAPI support
79-
pip install tusk-drift-python-sdk[fastapi]
80-
81-
# With Django support
82-
pip install tusk-drift-python-sdk[django]
83-
```
84-
8572
### Step 3: Initialize the SDK for your service
8673

8774
Refer to our [initialization guide](docs/initialization.md) to set up the SDK for your service.

docker-compose.test.yml

Lines changed: 0 additions & 56 deletions
This file was deleted.

drift/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232
from .instrumentation.fastapi import FastAPIInstrumentation
3333
from .instrumentation.flask import FlaskInstrumentation
3434
from .instrumentation.requests import RequestsInstrumentation
35+
from .instrumentation.urllib3 import Urllib3Instrumentation
36+
from .version import SDK_VERSION
3537

36-
__version__ = "0.1.0"
38+
__version__ = SDK_VERSION
3739

3840
__all__ = [
3941
# Core
@@ -61,6 +63,7 @@
6163
"FlaskInstrumentation",
6264
"FastAPIInstrumentation",
6365
"RequestsInstrumentation",
66+
"Urllib3Instrumentation",
6467
# Adapters
6568
"SpanExportAdapter",
6669
"ExportResult",

drift/core/batch_processor.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(
5757
self._config = config or BatchSpanProcessorConfig()
5858
self._queue: deque[CleanSpanData] = deque(maxlen=self._config.max_queue_size)
5959
self._lock = threading.Lock()
60+
self._condition = threading.Condition(self._lock)
6061
self._shutdown_event = threading.Event()
6162
self._export_thread: threading.Thread | None = None
6263
self._started = False
@@ -88,6 +89,9 @@ def stop(self, timeout: float | None = None) -> None:
8889
return
8990

9091
self._shutdown_event.set()
92+
# Wake up the export thread so it can see the shutdown event
93+
with self._condition:
94+
self._condition.notify_all()
9195

9296
if self._export_thread is not None:
9397
self._export_thread.join(timeout=timeout or self._config.export_timeout_seconds)
@@ -108,7 +112,7 @@ def add_span(self, span: CleanSpanData) -> bool:
108112
Returns:
109113
True if span was added, False if queue is full and span was dropped
110114
"""
111-
with self._lock:
115+
with self._condition:
112116
if len(self._queue) >= self._config.max_queue_size:
113117
self._dropped_spans += 1
114118
logger.warning(
@@ -121,16 +125,17 @@ def add_span(self, span: CleanSpanData) -> bool:
121125

122126
# Trigger immediate export if batch size reached
123127
if len(self._queue) >= self._config.max_export_batch_size:
124-
# Signal export thread to wake up (if using condition variable)
125-
pass
128+
self._condition.notify()
126129

127130
return True
128131

129132
def _export_loop(self) -> None:
130133
"""Background thread that periodically exports spans."""
131134
while not self._shutdown_event.is_set():
132-
# Wait for scheduled delay or shutdown
133-
self._shutdown_event.wait(timeout=self._config.scheduled_delay_seconds)
135+
# Wait for either: batch size reached, scheduled delay, or shutdown
136+
with self._condition:
137+
# Wait until batch is ready or timeout
138+
self._condition.wait(timeout=self._config.scheduled_delay_seconds)
134139

135140
if self._shutdown_event.is_set():
136141
break
@@ -141,7 +146,7 @@ def _export_batch(self) -> None:
141146
"""Export a batch of spans from the queue."""
142147
# Get batch of spans
143148
batch: list[CleanSpanData] = []
144-
with self._lock:
149+
with self._condition:
145150
while self._queue and len(batch) < self._config.max_export_batch_size:
146151
batch.append(self._queue.popleft())
147152

@@ -171,7 +176,7 @@ def _export_batch(self) -> None:
171176
def _force_flush(self) -> None:
172177
"""Force export all remaining spans in the queue."""
173178
while True:
174-
with self._lock:
179+
with self._condition:
175180
if not self._queue:
176181
break
177182

@@ -180,7 +185,7 @@ def _force_flush(self) -> None:
180185
@property
181186
def queue_size(self) -> int:
182187
"""Get the current queue size."""
183-
with self._lock:
188+
with self._condition:
184189
return len(self._queue)
185190

186191
@property

drift/core/communication/communicator.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,6 @@ def connect_sync(
253253
ConnectionError: If connection fails
254254
TimeoutError: If connection times out
255255
"""
256-
logger.info("[CONNECT_SYNC] Starting synchronous connection")
257256
# Determine address
258257
if connection_info:
259258
if "socketPath" in connection_info:
@@ -324,8 +323,6 @@ def connect_sync(
324323
if response.success:
325324
logger.debug("CLI acknowledged connection successfully")
326325
self._connected = True
327-
logger.info(f"[CONNECT_SYNC] Connection successful! Socket is: {self._socket}")
328-
logger.info(f"[CONNECT_SYNC] _connected={self._connected}, is_connected={self.is_connected}")
329326

330327
# Start background reader for CLI-initiated messages (like SetTimeTravel)
331328
self._start_background_reader()
@@ -344,8 +341,6 @@ def connect_sync(
344341
finally:
345342
calling_library_context.reset(context_token)
346343

347-
logger.info(f"[CONNECT_SYNC] Exiting connect_sync(). Socket still open: {self._socket is not None}")
348-
349344
async def disconnect(self) -> None:
350345
"""Disconnect from CLI."""
351346
self._cleanup()
@@ -517,8 +512,9 @@ async def send_unpatched_dependency_alert(
517512

518513
try:
519514
await self._send_protobuf_message(sdk_message)
520-
except Exception:
521-
pass # Alerts are non-critical
515+
except Exception as e:
516+
# Alerts are non-critical, just log at debug level
517+
logger.debug(f"Failed to send unpatched dependency alert: {e}")
522518

523519
async def _send_protobuf_message(self, message: SdkMessage) -> None:
524520
"""Send a protobuf message to CLI."""
@@ -775,9 +771,6 @@ def _cleanup(self) -> None:
775771
"""Clean up resources."""
776772
import traceback
777773

778-
logger.warning("[CLEANUP] _cleanup() called! Stack trace:")
779-
logger.warning("".join(traceback.format_stack()))
780-
781774
# Stop background reader thread
782775
self._stop_background_reader.set()
783776
if self._background_reader_thread and self._background_reader_thread.is_alive():
@@ -790,10 +783,10 @@ def _cleanup(self) -> None:
790783

791784
if self._socket:
792785
try:
793-
logger.warning("[CLEANUP] Closing socket")
794786
self._socket.close()
795-
except Exception:
796-
pass
787+
except OSError as e:
788+
# Socket may already be closed, which is fine
789+
logger.debug(f"Error closing socket during cleanup: {e}")
797790
self._socket = None
798791

799792
self._pending_requests.clear()

drift/core/config.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,17 @@ def _parse_service_config(data: dict[str, Any]) -> ServiceConfig:
135135

136136
def _parse_recording_config(data: dict[str, Any]) -> RecordingConfig:
137137
"""Parse recording configuration from raw dict."""
138+
# Validate sampling_rate type
139+
sampling_rate = data.get("sampling_rate")
140+
if sampling_rate is not None and not isinstance(sampling_rate, (int, float)):
141+
logger.warning(
142+
f"Invalid 'sampling_rate' in config: expected number, got {type(sampling_rate).__name__}. "
143+
"This value will be ignored."
144+
)
145+
sampling_rate = None
146+
138147
return RecordingConfig(
139-
sampling_rate=data.get("sampling_rate"),
148+
sampling_rate=sampling_rate,
140149
export_spans=data.get("export_spans"),
141150
enable_env_var_recording=data.get("enable_env_var_recording"),
142151
enable_analytics=data.get("enable_analytics"),

0 commit comments

Comments
 (0)