Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
version: "latest"

- name: Setup Python
run: uv python install 3.12
run: uv python install 3.9

- name: Install dependencies
run: uv sync --all-extras
Expand All @@ -44,7 +44,7 @@ jobs:
version: "latest"

- name: Setup Python
run: uv python install 3.12
run: uv python install 3.9

- name: Install dependencies
run: uv sync --all-extras
Expand All @@ -53,8 +53,11 @@ jobs:
run: uv run ty check drift/ tests/

test:
name: Unit Tests
name: Unit Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.14"]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -65,7 +68,7 @@ jobs:
version: "latest"

- name: Setup Python
run: uv python install 3.12
run: uv python install ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --all-extras
Expand All @@ -86,7 +89,7 @@ jobs:
version: "latest"

- name: Setup Python
run: uv python install 3.12
run: uv python install 3.9

- name: Install dependencies
run: uv sync --all-extras
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
version: "latest"

- name: Setup Python
run: uv python install 3.12
run: uv python install 3.9

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ For comprehensive guides and API reference, visit our [full documentation](https

## Requirements

- Python 3.12+
- Python 3.9+

Tusk Drift currently supports the following packages and versions:

Expand Down
2 changes: 1 addition & 1 deletion drift/core/batch_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def _export_batch(self) -> None:
finally:
loop.close()
else:
adapter.export_spans(batch) # type: ignore
adapter.export_spans(batch)

latency_ms = (time.monotonic() - start_time) * 1000
self._metrics.record_spans_exported(len(batch))
Expand Down
4 changes: 2 additions & 2 deletions drift/core/drift_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ def _init_auto_instrumentations(self) -> None:
pass

try:
import urllib3 # type: ignore[unresolved-import]
import urllib3

from ..instrumentation.urllib3 import Urllib3Instrumentation

Expand Down Expand Up @@ -452,7 +452,7 @@ def _init_auto_instrumentations(self) -> None:
pass

try:
import django # type: ignore[unresolved-import]
import django

from ..instrumentation.django import DjangoInstrumentation

Expand Down
5 changes: 4 additions & 1 deletion drift/core/resilience.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from enum import Enum
from typing import TypeVar

T = TypeVar("T")

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -51,7 +54,7 @@ def calculate_backoff_delay(
return delay


async def retry_async[T](
async def retry_async(
operation: Callable[[], Awaitable[T]],
config: RetryConfig | None = None,
retryable_exceptions: tuple[type[Exception], ...] = (Exception,),
Expand Down
4 changes: 2 additions & 2 deletions drift/core/span_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Any

from betterproto.lib.google.protobuf import Struct as ProtoStruct
Expand Down Expand Up @@ -114,7 +114,7 @@ def clean_span_to_proto(span: CleanSpanData) -> ProtoSpan:
is_root_span=span.is_root_span,
timestamp=datetime.fromtimestamp(
span.timestamp.seconds + span.timestamp.nanos / 1_000_000_000,
tz=UTC,
tz=timezone.utc,
),
duration=timedelta(
seconds=span.duration.seconds,
Expand Down
14 changes: 8 additions & 6 deletions drift/core/tracing/adapters/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
import gzip
import logging
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any, override
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Any

from typing_extensions import override

from ...resilience import (
CircuitBreaker,
Expand Down Expand Up @@ -270,7 +272,7 @@ def _transform_span_to_protobuf(self, clean_span: CleanSpanData) -> Any:

timestamp = datetime.fromtimestamp(
clean_span.timestamp.seconds + clean_span.timestamp.nanos / 1_000_000_000,
tz=UTC,
tz=timezone.utc,
)

duration = timedelta(
Expand Down Expand Up @@ -339,8 +341,8 @@ def convert_json_schema(sdk_schema: Any) -> Any:
type=type_value,
properties=proto_properties,
items=proto_items,
encoding=encoding_value, # type: ignore[arg-type]
decoded_type=decoded_type_value, # type: ignore[arg-type]
encoding=encoding_value,
decoded_type=decoded_type_value,
match_importance=sdk_schema.match_importance,
)

Expand Down Expand Up @@ -370,7 +372,7 @@ def convert_json_schema(sdk_schema: Any) -> Any:
timestamp=timestamp,
duration=duration,
is_root_span=clean_span.is_root_span,
metadata=metadata_struct, # type: ignore[arg-type]
metadata=metadata_struct,
)


Expand Down
8 changes: 5 additions & 3 deletions drift/core/tracing/adapters/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import logging
from collections import OrderedDict
from dataclasses import asdict
from datetime import UTC, datetime
from datetime import datetime, timezone
from pathlib import Path
from typing import TYPE_CHECKING, Any, override
from typing import TYPE_CHECKING, Any

from typing_extensions import override

from .base import ExportResult, SpanExportAdapter

Expand Down Expand Up @@ -63,7 +65,7 @@ def _get_or_create_file_path(self, trace_id: str) -> Path:
return self._trace_file_map[trace_id]

# Create new file with timestamp prefix
iso_timestamp = datetime.now(UTC).isoformat().replace(":", "-").replace(".", "-")
iso_timestamp = datetime.now(timezone.utc).isoformat().replace(":", "-").replace(".", "-")
file_path = self._base_directory / f"{iso_timestamp}_trace_{trace_id}.jsonl"
self._trace_file_map[trace_id] = file_path

Expand Down
4 changes: 3 additions & 1 deletion drift/core/tracing/adapters/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from __future__ import annotations

from typing import TYPE_CHECKING, override
from typing import TYPE_CHECKING

from typing_extensions import override

from .base import ExportResult, SpanExportAdapter

Expand Down
2 changes: 1 addition & 1 deletion drift/core/tracing/otel_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def _dict_to_transform_metadata(data: dict | None) -> TransformMetadata | None:
if isinstance(action_dict, dict):
actions.append(
TransformAction(
type=action_dict.get("type", "redact"), # type: ignore[arg-type]
type=action_dict.get("type", "redact"),
field=action_dict.get("field", ""),
reason=action_dict.get("reason", ""),
description=action_dict.get("description"),
Expand Down
2 changes: 2 additions & 0 deletions drift/instrumentation/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from types import ModuleType

Expand Down
4 changes: 2 additions & 2 deletions drift/instrumentation/datetime/instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from __future__ import annotations

import logging
from datetime import UTC, datetime
from datetime import datetime, timezone

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -45,7 +45,7 @@ def start_time_travel(timestamp: datetime | str | int | float, trace_id: str | N

# Parse timestamp to datetime if needed
if isinstance(timestamp, (int, float)):
dt = datetime.fromtimestamp(timestamp, tz=UTC)
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
elif isinstance(timestamp, str):
ts = timestamp.replace("Z", "+00:00")
dt = datetime.fromisoformat(ts)
Expand Down
2 changes: 1 addition & 1 deletion drift/instrumentation/django/e2e-tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-e /sdk # Mount point for drift SDK
Django>=5.0
Django>=4.2
requests>=2.32.5
gunicorn>=22.0.0

4 changes: 3 additions & 1 deletion drift/instrumentation/django/instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import logging
from types import ModuleType
from typing import TYPE_CHECKING, Any, override
from typing import TYPE_CHECKING, Any

from typing_extensions import override

logger = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion drift/instrumentation/django/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
logger = logging.getLogger(__name__)

if TYPE_CHECKING:
from django.http import HttpRequest, HttpResponse # type: ignore[import-not-found]
from django.http import HttpRequest, HttpResponse
from ...core.mode_utils import handle_record_mode
from ...core.tracing import TdSpanAttributes
from ...core.tracing.span_utils import CreateSpanOptions, SpanInfo, SpanUtils
Expand Down
4 changes: 2 additions & 2 deletions drift/instrumentation/e2e_common/Dockerfile.base
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Python E2E Test Base Image
#
# This base image contains:
# - Python 3.12
# - Python 3.9 (minimum supported version)
# - Tusk CLI (for running replay tests)
# - System utilities (curl, postgresql-client)
#
# Build this image before running e2e tests:
# docker build -t python-e2e-base:latest -f drift/instrumentation/e2e-common/Dockerfile.base .

FROM python:3.12-slim
FROM python:3.9-slim

# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
Expand Down
37 changes: 32 additions & 5 deletions drift/instrumentation/e2e_common/base_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
customize only the setup phase.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"""

from __future__ import annotations

import json
import os
import signal
import subprocess
import sys
import tempfile
import time
from pathlib import Path

Expand Down Expand Up @@ -44,6 +47,7 @@ class E2ETestRunnerBase:
def __init__(self, app_port: int = 8000):
self.app_port = app_port
self.app_process: subprocess.Popen | None = None
self.app_log_file: tempfile._TemporaryFileWrapper | None = None
self.exit_code = 0
self.expected_request_count: int | None = None

Expand Down Expand Up @@ -147,11 +151,17 @@ def record_traces(self) -> bool:
self.log("Starting application in RECORD mode...", Colors.GREEN)
env = {"TUSK_DRIFT_MODE": "RECORD", "PYTHONUNBUFFERED": "1"}

# Use a temporary file to capture app output for debugging.
# This avoids pipe buffer issues while still allowing diagnostics.
# Note: Can't use context manager here - file must stay open for subprocess
# and be cleaned up later in cleanup(). Using delete=False + manual unlink.
self.app_log_file = tempfile.NamedTemporaryFile(mode="w+", suffix=".log", delete=False) # noqa: SIM115

self.app_process = subprocess.Popen(
["python", "src/app.py"],
env={**os.environ, **env},
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
stdout=self.app_log_file,
stderr=subprocess.STDOUT,
text=True,
)

Expand All @@ -166,10 +176,18 @@ def record_traces(self) -> bool:
except TimeoutError:
self.log("Application failed to become ready", Colors.RED)
if self.app_process:
# Print app output for debugging
self.app_process.terminate()
stdout, _ = self.app_process.communicate(timeout=5)
self.log(f"App output: {stdout}", Colors.YELLOW)
try:
self.app_process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.app_process.kill()
self.app_process.wait()
# Read and display app output for debugging
if self.app_log_file:
self.app_log_file.flush()
self.app_log_file.seek(0)
app_output = self.app_log_file.read()
self.log(f"App output:\n{app_output}", Colors.YELLOW)
self.exit_code = 1
return False

Expand Down Expand Up @@ -385,6 +403,15 @@ def cleanup(self):
self.app_process.kill()
self.app_process.wait()

# Clean up app log file
if self.app_log_file:
try:
self.app_log_file.close()
os.unlink(self.app_log_file.name)
except OSError:
pass
self.app_log_file = None

# Traces are kept in container for inspection
self.log("Cleanup complete", Colors.GREEN)

Expand Down
4 changes: 3 additions & 1 deletion drift/instrumentation/fastapi/instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
from collections.abc import Callable
from functools import wraps
from types import ModuleType
from typing import TYPE_CHECKING, Any, override
from typing import TYPE_CHECKING, Any

from typing_extensions import override

logger = logging.getLogger(__name__)

Expand Down
6 changes: 4 additions & 2 deletions drift/instrumentation/flask/instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import logging
from collections.abc import Iterable
from types import ModuleType
from typing import TYPE_CHECKING, Any, override
from typing import TYPE_CHECKING, Any

from typing_extensions import override

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -69,5 +71,5 @@ def instrumented_wsgi_app(
transform_engine=transform_engine,
)

flask_class.wsgi_app = instrumented_wsgi_app # type: ignore
flask_class.wsgi_app = instrumented_wsgi_app
print("Flask instrumentation applied")
Loading