feat: add django tracing (#32418)

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
Michi
2025-06-04 10:31:32 +02:00
committed by GitHub
parent 0d63fff9a9
commit a335a5a50d
18 changed files with 967 additions and 21 deletions

View File

@@ -92,8 +92,16 @@ then
git checkout $latestReleaseTag
elif [[ "$POSTHOG_APP_TAG" = "latest" ]]
then
echo "Pulling latest from current branch: $(git branch --show-current)"
git pull
echo "Fetching latest changes from origin"
git fetch origin
current_branch=$(git branch --show-current)
if [ -n "$current_branch" ]; then
echo "Updating branch '$current_branch' to latest from origin"
git reset --hard origin/$current_branch
else
echo "On detached HEAD: $(git rev-parse --short HEAD)"
fi
echo "Now on commit: $(git rev-parse --short HEAD)"
elif [[ "$POSTHOG_APP_TAG" =~ ^[0-9a-f]{40}$ ]]
then
echo "Checking out specific commit hash: $POSTHOG_APP_TAG"

View File

@@ -9,6 +9,7 @@ import time
import digitalocean
import requests
import urllib3
DOMAIN = "posthog.cc"
@@ -21,7 +22,7 @@ class HobbyTester:
name=None,
region="sfo3",
image="ubuntu-22-04-x64",
size="s-4vcpu-8gb",
size="s-8vcpu-16gb",
release_tag="latest-release",
branch=None,
hostname=None,
@@ -67,10 +68,19 @@ class HobbyTester:
"sed -i \"s/#\\$nrconf{restart} = 'i';/\\$nrconf{restart} = 'a';/g\" /etc/needrestart/needrestart.conf \n"
"git clone https://github.com/PostHog/posthog.git \n"
"cd posthog \n"
f'echo "Using branch: {self.branch}" \n'
f"git checkout {self.branch} \n"
"CURRENT_COMMIT=$(git rev-parse HEAD) \n"
'echo "Current commit: $CURRENT_COMMIT" \n'
"cd .. \n"
f"chmod +x posthog/bin/deploy-hobby \n"
f"./posthog/bin/deploy-hobby {self.release_tag} {self.hostname} 1 \n"
f'if [ "{self.branch}" != "main" ] && [ "{self.branch}" != "master" ] && [ -n "{self.branch}" ]; then \n'
f' echo "Using commit hash for feature branch deployment" \n'
f" ./posthog/bin/deploy-hobby $CURRENT_COMMIT {self.hostname} 1 \n"
f"else \n"
f' echo "Installing PostHog version: {self.release_tag}" \n'
f" ./posthog/bin/deploy-hobby {self.release_tag} {self.hostname} 1 \n"
f"fi \n"
)
def block_until_droplet_is_started(self):
@@ -117,30 +127,38 @@ class HobbyTester:
self.droplet.create()
return self.droplet
def test_deployment(self, timeout=20, retry_interval=15):
def test_deployment(self, timeout=30, retry_interval=15):
if not self.hostname:
return
# timeout in minutes
# return true if success or false if failure
print("Attempting to reach the instance")
print(f"We will time out after {timeout} minutes")
# Suppress SSL warnings for staging Let's Encrypt certificates
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
url = f"https://{self.hostname}/_health"
start_time = datetime.datetime.now()
attempt = 1
while datetime.datetime.now() < start_time + datetime.timedelta(minutes=timeout):
print(f"Trying to connect... (attempt {attempt})")
try:
# verify is set False here because we are hitting the staging endoint for Let's Encrypt
# verify is set False here because we are hitting the staging endpoint for Let's Encrypt
# This endpoint doesn't have the strict rate limiting that the production endpoint has
# This mitigates the chances of getting throttled or banned
r = requests.get(url, verify=False)
r = requests.get(url, verify=False, timeout=10)
except Exception as e:
print(f"Host is probably not up. Received exception\n{e}")
print(f"Connection failed: {type(e).__name__}")
time.sleep(retry_interval)
attempt += 1
continue
if r.status_code == 200:
print("Success - received heartbeat from the instance")
return True
print("Instance not ready - sleeping")
print(f"Instance not ready (HTTP {r.status_code}) - sleeping")
time.sleep(retry_interval)
attempt += 1
print("Failure - we timed out before receiving a heartbeat")
return False

View File

@@ -8,6 +8,23 @@ export BILLING_SERVICE_URL=${BILLING_SERVICE_URL:-https://billing.dev.posthog.de
export HOG_HOOK_URL=${HOG_HOOK_URL:-http://localhost:3300/hoghook}
export API_QUERIES_PER_TEAM='{"1": 100}'
# OpenTelemetry Environment Variables
export OTEL_SERVICE_NAME="posthog-local-dev"
export OTEL_PYTHON_LOG_LEVEL="debug"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" # Collector's OTLP gRPC port is mapped to host
export OTEL_TRACES_EXPORTER="otlp"
export OTEL_METRICS_EXPORTER="none" # Explicitly disable if not used
export OTEL_LOGS_EXPORTER="none" # Explicitly disable if not used
export OTEL_PYTHON_DJANGO_INSTRUMENT="true"
export OTEL_PYTHON_DJANGO_MIDDLEWARE_POSITION="1"
if [[ "$*" == *"--enable-tracing"* ]]; then
export OTEL_SDK_DISABLED="false"
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
export OTEL_TRACES_SAMPLER_ARG="1"
else
export OTEL_SDK_DISABLED="true"
fi
if [ -f .env ]; then
set -o allexport
source .env

View File

@@ -9,4 +9,4 @@ export CLICKHOUSE_API_PASSWORD="apipass"
export CLICKHOUSE_APP_USER="app"
export CLICKHOUSE_APP_PASSWORD="apppass"
uvicorn --reload posthog.asgi:application --host 0.0.0.0 --log-level debug --reload-include "posthog/" --reload-include "ee/" --reload-include "products/"
uvicorn --reload posthog.asgi:application --host 0.0.0.0 --log-level debug --reload-include "posthog/" --reload-include "ee/" --reload-include "products/"

View File

@@ -153,6 +153,7 @@ services:
command: ./bin/docker-worker-celery --with-scheduler
restart: on-failure
environment: &worker_env
OTEL_SDK_DISABLED: 'true'
DISABLE_SECURE_SSL_REDIRECT: 'true'
IS_BEHIND_PROXY: 'true'
DATABASE_URL: 'postgres://posthog:posthog@db:5432/posthog'
@@ -379,3 +380,32 @@ services:
condition: service_healthy
kafka:
condition: service_started
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
container_name: otel-collector-local
command: [--config=/etc/otel-collector-config.yaml]
volumes:
- ./otel-collector-config.dev.yaml:/etc/otel-collector-config.yaml
ports:
- '4317:4317' # OTLP gRPC receiver (mapped to host)
- '4318:4318' # OTLP HTTP receiver (mapped to host)
- '13133:13133' # health_check extension
- '55679:55679' # zpages extension
depends_on:
- jaeger
networks:
- otel_network
jaeger:
image: jaegertracing/all-in-one:latest
container_name: jaeger-local
ports:
- '16686:16686' # Jaeger UI
- '14268:14268' # Accepts jaeger.thrift directly from clients (optional for this flow)
- '14250:14250' # Accepts model.proto (optional for this flow)
networks:
- otel_network
networks:
otel_network:
driver: bridge

View File

@@ -92,6 +92,7 @@ services:
- '2080:2080'
environment:
- PORT=2080
- OTEL_SDK_DISABLED=true
worker:
extends:
@@ -261,3 +262,19 @@ services:
condition: service_healthy
kafka:
condition: service_started
otel-collector:
extends:
file: docker-compose.base.yml
service: otel-collector
depends_on:
- jaeger
jaeger:
extends:
file: docker-compose.base.yml
service: jaeger
networks:
otel_network:
driver: bridge

View File

@@ -196,3 +196,19 @@ services:
condition: service_started
db:
condition: service_healthy
otel-collector:
extends:
file: docker-compose.base.yml
service: otel-collector
depends_on:
- jaeger
jaeger:
extends:
file: docker-compose.base.yml
service: jaeger
networks:
otel_network:
driver: bridge

View File

@@ -109,6 +109,9 @@ services:
SESSION_RECORDING_V2_S3_ENDPOINT: http://objectstorage:19000
OBJECT_STORAGE_ENABLED: true
ENCRYPTION_SALT_KEYS: $ENCRYPTION_SALT_KEYS
OTEL_SERVICE_NAME: 'posthog'
OTEL_EXPORTER_OTLP_ENDPOINT: ''
OTEL_SDK_DISABLED: 'true'
depends_on:
- db
- redis

View File

@@ -0,0 +1,28 @@
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317 # Collector receiving OTLP gRPC
http:
endpoint: 0.0.0.0:4318 # Collector receiving OTLP HTTP
exporters:
otlp: # Using the standard OTLP exporter
endpoint: 'jaeger-local:4317' # Sending OTLP gRPC to Jaeger
tls:
insecure: true # For local communication to Jaeger
extensions: # Declaring the extensions
health_check: # Default configuration is usually fine
zpages: # Default configuration is usually fine
processors:
batch:
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
extensions: [health_check, zpages] # Enabling the declared extensions

File diff suppressed because one or more lines are too long

View File

@@ -40,7 +40,12 @@ class PostHogConfig(AppConfig):
# Instead, we configure self-capture with `self_capture_wrapper()` in posthog/asgi.py - see that file
# Self-capture for WSGI is initialized here
posthoganalytics.disabled = True
if settings.SERVER_GATEWAY_INTERFACE == "WSGI":
logger.info(
"posthog_config_ready",
settings_debug=settings.DEBUG,
server_gateway_interface=os.environ.get("SERVER_GATEWAY_INTERFACE"),
)
if os.environ.get("SERVER_GATEWAY_INTERFACE") == "WSGI":
async_to_sync(initialize_self_capture_api_token)()
# log development server launch to posthog
if os.getenv("RUN_MAIN") == "true":

View File

@@ -1,11 +1,28 @@
import os
# Django Imports
from django.conf import settings
from django.core.asgi import get_asgi_application
from django.http.response import HttpResponse
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "posthog.settings")
os.environ.setdefault("SERVER_GATEWAY_INTERFACE", "ASGI")
# Structlog Import
import structlog
# PostHog OpenTelemetry Initialization
from posthog.otel_instrumentation import initialize_otel
initialize_otel() # Initialize OpenTelemetry first
# Get a structlog logger for asgi.py's own messages
logger = structlog.get_logger(__name__)
os.environ["DJANGO_SETTINGS_MODULE"] = "posthog.settings"
# Try to ensure SERVER_GATEWAY_INTERFACE is fresh for the child process
if "SERVER_GATEWAY_INTERFACE" in os.environ:
del os.environ["SERVER_GATEWAY_INTERFACE"] # Delete if inherited
os.environ["SERVER_GATEWAY_INTERFACE"] = "ASGI" # Set definitively
# Django doesn't support lifetime requests and raises an exception

View File

@@ -0,0 +1,150 @@
import logging
import os
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.django import DjangoInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor
from opentelemetry.instrumentation.kafka import KafkaInstrumentor
from opentelemetry.instrumentation.aiokafka import AIOKafkaInstrumentor
import structlog
# Get a structlog logger for this module's own messages
logger = structlog.get_logger(__name__)
def _otel_django_request_hook(span, request):
if span and span.is_recording():
actual_path = request.path
http_method = request.method
span.set_attribute("http.method", http_method)
span.set_attribute("http.url", actual_path)
# span.update_name(f"{http_method} {actual_path}") # Use with caution - high cardinality
def _otel_django_response_hook(span, request, response):
if span and span.is_recording():
span.set_attribute("http.status_code", response.status_code)
def initialize_otel():
# --- BEGIN FORCED OTEL DEBUG LOGGING ---
otel_python_log_level_env = os.environ.get("OTEL_PYTHON_LOG_LEVEL", "info").lower()
effective_log_level = logging.DEBUG if otel_python_log_level_env == "debug" else logging.INFO
root_logger = logging.getLogger()
# Set root logger level only if we are making it more verbose than it might already be.
# Or if it's not set (None), then set it.
# This avoids overriding a potentially more restrictive level set elsewhere.
if root_logger.level == logging.NOTSET or effective_log_level < root_logger.level:
root_logger.setLevel(effective_log_level)
django_instr_logger = logging.getLogger("opentelemetry.instrumentation.django")
django_instr_logger.setLevel(logging.DEBUG) # Force this to DEBUG
django_instr_logger.propagate = True # Ensure its messages go to root handlers for structlog processing
logger.info(
"otel_sdk_logging_config_from_instrumentation_module",
note="Configured OTel SDK log levels. Formatting via django-structlog.",
root_logger_current_level=logging.getLevelName(root_logger.level),
root_logger_target_level=logging.getLevelName(effective_log_level),
django_instrumentor_logger_target_level="DEBUG",
otel_python_log_level_env=otel_python_log_level_env,
)
# --- END FORCED OTEL DEBUG LOGGING ---
if os.environ.get("OTEL_SDK_DISABLED", "false").lower() != "true":
service_name = os.environ.get("OTEL_SERVICE_NAME", "posthog-django-default")
resource = Resource.create(attributes={"service.name": service_name})
# Let OpenTelemetry SDK handle sampling configuration via OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG
# This allows parentbased_traceidratio and other standard sampler types
sampler_type = os.environ.get("OTEL_TRACES_SAMPLER", "parentbased_traceidratio") # Respect parent decisions
sampler_arg = os.environ.get("OTEL_TRACES_SAMPLER_ARG", "0")
logger.info(
"otel_sampler_configured",
sampler_type=sampler_type,
sampler_arg=sampler_arg if sampler_arg else "default",
note="Using OpenTelemetry standard sampling configuration",
source_module="otel_instrumentation",
)
provider = TracerProvider(resource=resource)
otlp_exporter = OTLPSpanExporter() # Assumes OTLP endpoint is configured via env vars
processor = BatchSpanProcessor(otlp_exporter)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
logger.info(
"otel_core_components_initialized_successfully",
service_name=service_name,
source_module="otel_instrumentation",
)
try:
DjangoInstrumentor().instrument(
tracer_provider=provider,
request_hook=_otel_django_request_hook,
response_hook=_otel_django_response_hook,
)
logger.info("otel_instrumentation_attempt", instrumentor="DjangoInstrumentor", status="success")
except Exception as e:
logger.exception(
"otel_instrumentation_attempt", instrumentor="DjangoInstrumentor", status="error", exc_info=e
)
try:
RedisInstrumentor().instrument(tracer_provider=provider)
logger.info("otel_instrumentation_attempt", instrumentor="RedisInstrumentor", status="success")
except Exception as e:
logger.exception(
"otel_instrumentation_attempt", instrumentor="RedisInstrumentor", status="error", exc_info=e
)
try:
PsycopgInstrumentor().instrument(
tracer_provider=provider, enable_commenter=True, commenter_options={"opentelemetry_values": True}
)
logger.info(
"otel_instrumentation_attempt",
instrumentor="PsycopgInstrumentor",
status="success",
note="SQLCommenter enabled for diagnostics",
)
except Exception as e:
logger.exception(
"otel_instrumentation_attempt", instrumentor="PsycopgInstrumentor", status="error", exc_info=e
)
try:
KafkaInstrumentor().instrument(tracer_provider=provider)
logger.info("otel_instrumentation_attempt", instrumentor="KafkaInstrumentor", status="success")
except Exception as e:
logger.exception(
"otel_instrumentation_attempt", instrumentor="KafkaInstrumentor", status="error", exc_info=e
)
try:
AIOKafkaInstrumentor().instrument(tracer_provider=provider)
logger.info("otel_instrumentation_attempt", instrumentor="AIOKafkaInstrumentor", status="success")
except Exception as e:
logger.exception(
"otel_instrumentation_attempt", instrumentor="AIOKafkaInstrumentor", status="error", exc_info=e
)
logger.info(
"otel_manual_init_status_from_instrumentation_module",
service_name=service_name,
detail="OpenTelemetry manually initialized with hooks (no manual middleware entry in settings.MIDDLEWARE)",
)
else:
logger.info(
"otel_manual_init_status_from_instrumentation_module",
status="disabled",
reason="OTEL_SDK_DISABLED environment variable is set to true",
)

View File

@@ -19,5 +19,5 @@ if SKIP_SERVICE_VERSION_REQUIREMENTS and not (TEST or DEBUG):
SERVICE_VERSION_REQUIREMENTS = [
ServiceVersionRequirement(service="postgresql", supported_version=">=11.0.0,<=14.1.0"),
ServiceVersionRequirement(service="redis", supported_version=">=5.0.0,<=6.3.0"),
ServiceVersionRequirement(service="clickhouse", supported_version=">=22.8.0,<24.0.0"),
ServiceVersionRequirement(service="clickhouse", supported_version=">=22.8.0,<=24.8.7"),
]

View File

@@ -0,0 +1,347 @@
# posthog/test/test_otel_instrumentation.py
from unittest import mock
import logging
import os
from posthog.otel_instrumentation import initialize_otel, _otel_django_request_hook, _otel_django_response_hook
from posthog.test.base import BaseTest
class TestOtelInstrumentation(BaseTest):
def setUp(self):
super().setUp()
# Store original levels to restore them after tests
self.original_root_level = logging.getLogger().level
self.original_django_otel_logger_level = logging.getLogger("opentelemetry.instrumentation.django").level
self.original_django_otel_logger_propagate = logging.getLogger("opentelemetry.instrumentation.django").propagate
# Set to a known, possibly different, state before each test that calls initialize_otel
logging.getLogger().setLevel(logging.WARNING)
# For the django_instr_logger, initialize_otel always sets it to DEBUG and propagate=True
# So, we don't need to change its initial state as much as ensure it's reset.
logging.getLogger("opentelemetry.instrumentation.django").setLevel(logging.WARNING)
logging.getLogger("opentelemetry.instrumentation.django").propagate = False
def tearDown(self):
# Restore original levels and propagation
logging.getLogger().setLevel(self.original_root_level)
django_otel_logger = logging.getLogger("opentelemetry.instrumentation.django")
django_otel_logger.setLevel(self.original_django_otel_logger_level)
django_otel_logger.propagate = self.original_django_otel_logger_propagate
# Clear any potentially set OTel provider to avoid state leakage between tests
# if initialize_otel was called and set a global provider.
from opentelemetry import trace
trace._TRACER_PROVIDER = None
super().tearDown()
@mock.patch("posthog.otel_instrumentation.AIOKafkaInstrumentor")
@mock.patch("posthog.otel_instrumentation.KafkaInstrumentor")
@mock.patch("posthog.otel_instrumentation.PsycopgInstrumentor")
@mock.patch("posthog.otel_instrumentation.RedisInstrumentor")
@mock.patch("posthog.otel_instrumentation.DjangoInstrumentor")
@mock.patch("posthog.otel_instrumentation.BatchSpanProcessor")
@mock.patch("posthog.otel_instrumentation.OTLPSpanExporter")
@mock.patch("posthog.otel_instrumentation.TracerProvider")
@mock.patch("posthog.otel_instrumentation.Resource")
@mock.patch("opentelemetry.trace.set_tracer_provider")
@mock.patch.dict(
os.environ,
{
"OTEL_SERVICE_NAME": "test-service",
"OTEL_SDK_DISABLED": "false",
"OTEL_PYTHON_LOG_LEVEL": "debug",
},
clear=True,
)
@mock.patch("posthog.otel_instrumentation.logger")
def test_initialize_otel_enabled_and_configured(
self,
mock_structlog_logger,
mock_set_tracer_provider,
mock_resource_cls,
mock_tracer_provider_cls,
mock_otlp_exporter_cls,
mock_batch_processor_cls,
mock_django_instrumentor_cls,
mock_redis_instrumentor_cls,
mock_psycopg_instrumentor_cls,
mock_kafka_instrumentor_cls,
mock_aio_kafka_instrumentor_cls,
):
# Arrange
mock_resource_instance = mock.Mock()
mock_resource_cls.create.return_value = mock_resource_instance
mock_provider_instance = mock.Mock()
mock_tracer_provider_cls.return_value = mock_provider_instance
mock_django_instrumentor_instance = mock.Mock()
mock_django_instrumentor_cls.return_value = mock_django_instrumentor_instance
mock_redis_instrumentor_instance = mock.Mock()
mock_redis_instrumentor_cls.return_value = mock_redis_instrumentor_instance
mock_psycopg_instrumentor_instance = mock.Mock()
mock_psycopg_instrumentor_cls.return_value = mock_psycopg_instrumentor_instance
mock_kafka_instrumentor_instance = mock.Mock()
mock_kafka_instrumentor_cls.return_value = mock_kafka_instrumentor_instance
mock_aio_kafka_instrumentor_instance = mock.Mock()
mock_aio_kafka_instrumentor_cls.return_value = mock_aio_kafka_instrumentor_instance
# Act
initialize_otel()
# Assert
mock_resource_cls.create.assert_called_once_with(attributes={"service.name": "test-service"})
# Check TracerProvider call with sampler
self.assertEqual(mock_tracer_provider_cls.call_count, 1)
call_args = mock_tracer_provider_cls.call_args
self.assertEqual(call_args[1]["resource"], mock_resource_instance)
# No longer passing sampler manually - OpenTelemetry SDK handles it via env vars
self.assertNotIn("sampler", call_args[1])
mock_otlp_exporter_cls.assert_called_once_with()
mock_batch_processor_cls.assert_called_once_with(mock_otlp_exporter_cls.return_value)
mock_provider_instance.add_span_processor.assert_called_once_with(mock_batch_processor_cls.return_value)
mock_set_tracer_provider.assert_called_once_with(mock_provider_instance)
mock_django_instrumentor_cls.assert_called_once_with()
mock_django_instrumentor_instance.instrument.assert_called_once()
instrument_call_args = mock_django_instrumentor_instance.instrument.call_args
self.assertEqual(instrument_call_args[1]["tracer_provider"], mock_provider_instance)
self.assertEqual(instrument_call_args[1]["request_hook"], _otel_django_request_hook)
self.assertEqual(instrument_call_args[1]["response_hook"], _otel_django_response_hook)
# Assert RedisInstrumentor call
mock_redis_instrumentor_cls.assert_called_once_with()
mock_redis_instrumentor_instance.instrument.assert_called_once_with(tracer_provider=mock_provider_instance)
# Assert PsycopgInstrumentor call
mock_psycopg_instrumentor_cls.assert_called_once_with()
mock_psycopg_instrumentor_instance.instrument.assert_called_once_with(
tracer_provider=mock_provider_instance,
enable_commenter=True,
commenter_options={"opentelemetry_values": True},
)
# Assert KafkaInstrumentor call
mock_kafka_instrumentor_cls.assert_called_once_with()
mock_kafka_instrumentor_instance.instrument.assert_called_once_with(tracer_provider=mock_provider_instance)
# Assert AIOKafkaInstrumentor call
mock_aio_kafka_instrumentor_cls.assert_called_once_with()
mock_aio_kafka_instrumentor_instance.instrument.assert_called_once_with(tracer_provider=mock_provider_instance)
# Check structlog logging calls
found_init_success_log = False
found_sdk_config_log = False
found_sampler_config_log = False
for call_args_tuple in mock_structlog_logger.info.call_args_list:
args, kwargs = call_args_tuple
event_name = args[0] if args else None
if event_name == "otel_manual_init_status_from_instrumentation_module":
if kwargs.get("service_name") == "test-service":
found_init_success_log = True
elif event_name == "otel_sdk_logging_config_from_instrumentation_module":
found_sdk_config_log = True
elif event_name == "otel_sampler_configured":
# Check for new env var based logging instead of sampler object properties
if kwargs.get("sampler_type") == "parentbased_traceidratio" and kwargs.get("sampler_arg") == "0":
found_sampler_config_log = True
self.assertTrue(found_init_success_log, "Expected OTel initialization success log not found or incorrect.")
self.assertTrue(found_sdk_config_log, "Expected OTel SDK logging configuration log not found.")
self.assertTrue(found_sampler_config_log, "Expected OTel sampler configuration log not found or incorrect.")
# Check standard library logger configurations
root_logger = logging.getLogger()
self.assertEqual(root_logger.level, logging.DEBUG) # OTEL_PYTHON_LOG_LEVEL is debug
django_otel_lib_logger = logging.getLogger("opentelemetry.instrumentation.django")
self.assertEqual(django_otel_lib_logger.level, logging.DEBUG) # Always set to DEBUG
self.assertTrue(django_otel_lib_logger.propagate)
@mock.patch("posthog.otel_instrumentation.AIOKafkaInstrumentor")
@mock.patch("posthog.otel_instrumentation.KafkaInstrumentor")
@mock.patch("posthog.otel_instrumentation.PsycopgInstrumentor")
@mock.patch("posthog.otel_instrumentation.RedisInstrumentor")
@mock.patch("posthog.otel_instrumentation.DjangoInstrumentor")
@mock.patch("posthog.otel_instrumentation.logger")
@mock.patch.dict(os.environ, {"OTEL_SDK_DISABLED": "true", "OTEL_PYTHON_LOG_LEVEL": "info"}, clear=True)
def test_initialize_otel_disabled(
self,
mock_structlog_logger,
mock_django_instrumentor_cls,
mock_redis_instrumentor_cls,
mock_psycopg_instrumentor_cls,
mock_kafka_instrumentor_cls,
mock_aio_kafka_instrumentor_cls,
):
# Act
initialize_otel()
# Assert
mock_django_instrumentor_cls.return_value.instrument.assert_not_called()
mock_redis_instrumentor_cls.return_value.instrument.assert_not_called()
mock_psycopg_instrumentor_cls.return_value.instrument.assert_not_called()
mock_kafka_instrumentor_cls.return_value.instrument.assert_not_called()
mock_aio_kafka_instrumentor_cls.return_value.instrument.assert_not_called()
found_disabled_log = False
for call_args_tuple in mock_structlog_logger.info.call_args_list:
args, kwargs = call_args_tuple
event_name = args[0] if args else None
if event_name == "otel_manual_init_status_from_instrumentation_module":
if kwargs.get("status") == "disabled":
found_disabled_log = True
self.assertTrue(found_disabled_log, "Expected OTel disabled status log not found or incorrect.")
# Check standard library logger configurations (logging setup still happens)
root_logger = logging.getLogger()
self.assertEqual(root_logger.level, logging.INFO) # OTEL_PYTHON_LOG_LEVEL is info
django_otel_lib_logger = logging.getLogger("opentelemetry.instrumentation.django")
self.assertEqual(django_otel_lib_logger.level, logging.DEBUG) # Always set to DEBUG
self.assertTrue(django_otel_lib_logger.propagate)
def test_otel_django_request_hook(self):
mock_span = mock.Mock()
mock_span.is_recording.return_value = True
mock_request = mock.Mock()
mock_request.path = "/test/path"
mock_request.method = "GET"
_otel_django_request_hook(mock_span, mock_request)
mock_span.set_attribute.assert_any_call("http.method", "GET")
mock_span.set_attribute.assert_any_call("http.url", "/test/path")
self.assertEqual(mock_span.set_attribute.call_count, 2)
def test_otel_django_request_hook_not_recording(self):
mock_span = mock.Mock()
mock_span.is_recording.return_value = False
mock_request = mock.Mock()
_otel_django_request_hook(mock_span, mock_request)
mock_span.set_attribute.assert_not_called()
def test_otel_django_response_hook(self):
mock_span = mock.Mock()
mock_span.is_recording.return_value = True
mock_request = mock.Mock() # Not used by this hook's logic
mock_response = mock.Mock()
mock_response.status_code = 200
_otel_django_response_hook(mock_span, mock_request, mock_response)
mock_span.set_attribute.assert_called_once_with("http.status_code", 200)
def test_otel_django_response_hook_not_recording(self):
mock_span = mock.Mock()
mock_span.is_recording.return_value = False
mock_request = mock.Mock()
mock_response = mock.Mock()
_otel_django_response_hook(mock_span, mock_request, mock_response)
mock_span.set_attribute.assert_not_called()
@mock.patch("posthog.otel_instrumentation.AIOKafkaInstrumentor")
@mock.patch("posthog.otel_instrumentation.KafkaInstrumentor")
@mock.patch("posthog.otel_instrumentation.PsycopgInstrumentor")
@mock.patch("posthog.otel_instrumentation.RedisInstrumentor")
@mock.patch("posthog.otel_instrumentation.DjangoInstrumentor")
@mock.patch("posthog.otel_instrumentation.BatchSpanProcessor")
@mock.patch("posthog.otel_instrumentation.OTLPSpanExporter")
@mock.patch("posthog.otel_instrumentation.TracerProvider")
@mock.patch("posthog.otel_instrumentation.Resource")
@mock.patch("opentelemetry.trace.set_tracer_provider")
@mock.patch.dict(
os.environ,
{
"OTEL_SERVICE_NAME": "test-service-custom-sample",
"OTEL_SDK_DISABLED": "false",
"OTEL_PYTHON_LOG_LEVEL": "info",
"OTEL_TRACES_SAMPLER": "parentbased_traceidratio",
"OTEL_TRACES_SAMPLER_ARG": "0.5",
},
clear=True,
)
@mock.patch("posthog.otel_instrumentation.logger")
def test_initialize_otel_with_custom_sampling_ratio(
self,
mock_structlog_logger,
mock_set_tracer_provider,
mock_resource_cls,
mock_tracer_provider_cls,
mock_otlp_exporter_cls,
mock_batch_processor_cls,
mock_django_instrumentor_cls,
mock_redis_instrumentor_cls,
mock_psycopg_instrumentor_cls,
mock_kafka_instrumentor_cls,
mock_aio_kafka_instrumentor_cls,
):
# Arrange
mock_resource_instance = mock.Mock()
mock_resource_cls.create.return_value = mock_resource_instance
mock_provider_instance = mock.Mock()
mock_tracer_provider_cls.return_value = mock_provider_instance
# Added mock instantiations for kafka instrumentors
mock_django_instrumentor_instance = mock.Mock()
mock_django_instrumentor_cls.return_value = mock_django_instrumentor_instance
mock_redis_instrumentor_instance = mock.Mock()
mock_redis_instrumentor_cls.return_value = mock_redis_instrumentor_instance
mock_psycopg_instrumentor_instance = mock.Mock()
mock_psycopg_instrumentor_cls.return_value = mock_psycopg_instrumentor_instance
mock_kafka_instrumentor_instance = mock.Mock()
mock_kafka_instrumentor_cls.return_value = mock_kafka_instrumentor_instance
mock_aio_kafka_instrumentor_instance = mock.Mock()
mock_aio_kafka_instrumentor_cls.return_value = mock_aio_kafka_instrumentor_instance
# Act
initialize_otel()
# Assert
mock_resource_cls.create.assert_called_once_with(attributes={"service.name": "test-service-custom-sample"})
# Check TracerProvider call with sampler
self.assertEqual(mock_tracer_provider_cls.call_count, 1)
call_args = mock_tracer_provider_cls.call_args
self.assertEqual(call_args[1]["resource"], mock_resource_instance)
# No longer passing sampler manually - OpenTelemetry SDK handles it via env vars
self.assertNotIn("sampler", call_args[1])
mock_set_tracer_provider.assert_called_once_with(mock_provider_instance)
# Check for sampler configured log
found_sampler_config_log = False
for call_args_tuple in mock_structlog_logger.info.call_args_list:
args, kwargs = call_args_tuple
event_name = args[0] if args else None
if event_name == "otel_sampler_configured":
if kwargs.get("sampler_type") == "parentbased_traceidratio" and kwargs.get("sampler_arg") == "0.5":
found_sampler_config_log = True
self.assertTrue(
found_sampler_config_log, "Expected OTel sampler configuration log with custom ratio not found."
)
# Assert KafkaInstrumentor call
mock_kafka_instrumentor_cls.assert_called_once_with()
mock_kafka_instrumentor_instance.instrument.assert_called_once_with(tracer_provider=mock_provider_instance)
# Assert AIOKafkaInstrumentor call
mock_aio_kafka_instrumentor_cls.assert_called_once_with()
mock_aio_kafka_instrumentor_instance.instrument.assert_called_once_with(tracer_provider=mock_provider_instance)

View File

@@ -9,8 +9,13 @@ https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
import os
# PostHog OpenTelemetry Initialization
from posthog.otel_instrumentation import initialize_otel
from django.core.wsgi import get_wsgi_application
initialize_otel()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "posthog.settings")
os.environ.setdefault("SERVER_GATEWAY_INTERFACE", "WSGI")

View File

@@ -139,6 +139,14 @@ dependencies = [
"elevenlabs>=1.58.1",
"django-oauth-toolkit>=3.0.1",
"langchain-perplexity>=0.1.1",
"opentelemetry-sdk==1.33.1",
"opentelemetry-instrumentation-django==0.54b1",
"opentelemetry-instrumentation-asgi==0.54b1",
"opentelemetry-instrumentation-redis==0.54b1",
"opentelemetry-instrumentation-psycopg==0.54b1",
"opentelemetry-instrumentation-aiokafka==0.54b1",
"opentelemetry-instrumentation-kafka-python==0.54b1",
"opentelemetry-exporter-otlp-proto-grpc==1.33.1",
]
[dependency-groups]

274
uv.lock generated
View File

@@ -1068,6 +1068,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/ed/770d6293361753934c108fd9cf195a1d84143184c7fafcc9927c12b405b8/deltalake-0.25.2-cp39-abi3-win_amd64.whl", hash = "sha256:4218c00bd9b6b69a93847afe2084532b998fdb171c205e014b0e7d7c4c7f84f6", size = 37576600, upload-time = "2025-02-25T20:40:17.18Z" },
]
[[package]]
name = "deprecated"
version = "1.2.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2e/a3/53e7d78a6850ffdd394d7048a31a6f14e44900adedf190f9a165f6b69439/deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d", size = 2977612, upload-time = "2024-11-15T14:42:06.39Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/8f/c7f227eb42cfeaddce3eb0c96c60cbca37797fa7b34f8e1aeadf6c5c0983/Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320", size = 9941, upload-time = "2024-11-15T14:42:03.315Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
@@ -2173,6 +2185,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "importlib-metadata"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/5a/392426ddb5edfebfcb232ab7a47e4a827aa1d5b5267a5c20c448615feaa9/importlib_metadata-7.0.0.tar.gz", hash = "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7", size = 54280, upload-time = "2023-12-03T17:42:16.831Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/26/9777cfe0cdc8181a32eaf542f4a2a435e5aba5dd38f41cfc0a532dc51027/importlib_metadata-7.0.0-py3-none-any.whl", hash = "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67", size = 23175, upload-time = "2023-12-03T17:42:14.834Z" },
]
[[package]]
name = "infi-clickhouse-orm"
version = "2.1.0.post19"
@@ -3002,6 +3026,231 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/94/a59521de836ef0da54aaf50da6c4da8fb4072fb3053fa71f052fd9399e7a/openpyxl-3.1.2-py2.py3-none-any.whl", hash = "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5", size = 249985, upload-time = "2023-03-11T16:58:36.257Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ name = "importlib-metadata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9a/8d/1f5a45fbcb9a7d87809d460f09dc3399e3fbd31d7f3e14888345e9d29951/opentelemetry_api-1.33.1.tar.gz", hash = "sha256:1c6055fc0a2d3f23a50c7e17e16ef75ad489345fd3df1f8b8af7c0bbf8a109e8", size = 65002, upload-time = "2025-05-16T18:52:41.146Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/44/4c45a34def3506122ae61ad684139f0bbc4e00c39555d4f7e20e0e001c8a/opentelemetry_api-1.33.1-py3-none-any.whl", hash = "sha256:4db83ebcf7ea93e64637ec6ee6fabee45c5cbe4abd9cf3da95c43828ddb50b83", size = 65771, upload-time = "2025-05-16T18:52:17.419Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/18/a1ec9dcb6713a48b4bdd10f1c1e4d5d2489d3912b80d2bcc059a9a842836/opentelemetry_exporter_otlp_proto_common-1.33.1.tar.gz", hash = "sha256:c57b3fa2d0595a21c4ed586f74f948d259d9949b58258f11edb398f246bec131", size = 20828, upload-time = "2025-05-16T18:52:43.795Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/09/52/9bcb17e2c29c1194a28e521b9d3f2ced09028934c3c52a8205884c94b2df/opentelemetry_exporter_otlp_proto_common-1.33.1-py3-none-any.whl", hash = "sha256:b81c1de1ad349785e601d02715b2d29d6818aed2c809c20219f3d1f20b038c36", size = 18839, upload-time = "2025-05-16T18:52:22.447Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-grpc"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ name = "googleapis-common-protos" },
{ name = "grpcio" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d8/5f/75ef5a2a917bd0e6e7b83d3fb04c99236ee958f6352ba3019ea9109ae1a6/opentelemetry_exporter_otlp_proto_grpc-1.33.1.tar.gz", hash = "sha256:345696af8dc19785fac268c8063f3dc3d5e274c774b308c634f39d9c21955728", size = 22556, upload-time = "2025-05-16T18:52:44.76Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/ec/6047e230bb6d092c304511315b13893b1c9d9260044dd1228c9d48b6ae0e/opentelemetry_exporter_otlp_proto_grpc-1.33.1-py3-none-any.whl", hash = "sha256:7e8da32c7552b756e75b4f9e9c768a61eb47dee60b6550b37af541858d669ce1", size = 18591, upload-time = "2025-05-16T18:52:23.772Z" },
]
[[package]]
name = "opentelemetry-instrumentation"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "packaging" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c3/fd/5756aea3fdc5651b572d8aef7d94d22a0a36e49c8b12fcb78cb905ba8896/opentelemetry_instrumentation-0.54b1.tar.gz", hash = "sha256:7658bf2ff914b02f246ec14779b66671508125c0e4227361e56b5ebf6cef0aec", size = 28436, upload-time = "2025-05-16T19:03:22.223Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/89/0790abc5d9c4fc74bd3e03cb87afe2c820b1d1a112a723c1163ef32453ee/opentelemetry_instrumentation-0.54b1-py3-none-any.whl", hash = "sha256:a4ae45f4a90c78d7006c51524f57cd5aa1231aef031eae905ee34d5423f5b198", size = 31019, upload-time = "2025-05-16T19:02:15.611Z" },
]
[[package]]
name = "opentelemetry-instrumentation-aiokafka"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d8/64/e37ecc02d01f5db44750d9460e3e244d5d8a4866e355df9fb85a5a0ae519/opentelemetry_instrumentation_aiokafka-0.54b1.tar.gz", hash = "sha256:977d733bf21f5891f2a0830d02a996d8cf111f00b03a76d419803f8d208b48a4", size = 12521, upload-time = "2025-05-16T19:03:28.466Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/45/f0d22618c546b4fe9050a05981bf60dd2a7170c0f4a6bc921341571e3e9d/opentelemetry_instrumentation_aiokafka-0.54b1-py3-none-any.whl", hash = "sha256:af1f69b5c3399b25ac574b2dceebb9a0c6fdf18f3d61850ccc4c69e8e81f8ee1", size = 12128, upload-time = "2025-05-16T19:02:20.731Z" },
]
[[package]]
name = "opentelemetry-instrumentation-asgi"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/f7/a3377f9771947f4d3d59c96841d3909274f446c030dbe8e4af871695ddee/opentelemetry_instrumentation_asgi-0.54b1.tar.gz", hash = "sha256:ab4df9776b5f6d56a78413c2e8bbe44c90694c67c844a1297865dc1bd926ed3c", size = 24230, upload-time = "2025-05-16T19:03:30.234Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/24/7a6f0ae79cae49927f528ecee2db55a5bddd87b550e310ce03451eae7491/opentelemetry_instrumentation_asgi-0.54b1-py3-none-any.whl", hash = "sha256:84674e822b89af563b283a5283c2ebb9ed585d1b80a1c27fb3ac20b562e9f9fc", size = 16338, upload-time = "2025-05-16T19:02:22.808Z" },
]
[[package]]
name = "opentelemetry-instrumentation-dbapi"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7b/b7/b74e2c7c858cde8909516cbe77cb0e841167d38795c90df524d84440e1f1/opentelemetry_instrumentation_dbapi-0.54b1.tar.gz", hash = "sha256:69421c36994114040d197f7e846c01869d663084c6c2025e85b2d6cfce2f8299", size = 14145, upload-time = "2025-05-16T19:03:40.074Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/6a/98d409ae5ca60ae4e41295a42256d81bb96bd5a7a386ca0343e27494d53d/opentelemetry_instrumentation_dbapi-0.54b1-py3-none-any.whl", hash = "sha256:21bc20cd878a78bf44bab686e9679cef1eed77e53c754c0a09f0ca49f5fd0283", size = 12450, upload-time = "2025-05-16T19:02:36.041Z" },
]
[[package]]
name = "opentelemetry-instrumentation-django"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-instrumentation-wsgi" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/93/8d194bda118fc4c369b9a3091c39eec384137b46f33421272359883c53d9/opentelemetry_instrumentation_django-0.54b1.tar.gz", hash = "sha256:38414f989f60e9dba82928e13f6a20a26baf5cc700f1d891f27e0703ca577802", size = 24866, upload-time = "2025-05-16T19:03:41.183Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/75/1b0ae1b8b7d6a85d5d54e8092c84b18669bd5da6f5ceb3410047674db3c0/opentelemetry_instrumentation_django-0.54b1-py3-none-any.whl", hash = "sha256:462fbd577991021f56152df21ca1fdcd7c4abdc10dd44254a44d515b8e3d61ca", size = 19541, upload-time = "2025-05-16T19:02:37.4Z" },
]
[[package]]
name = "opentelemetry-instrumentation-kafka-python"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b2/1c/232ffeb76dd519d82c6b0f1b28dc33f6583f3a90b35dd3360179d46e0c72/opentelemetry_instrumentation_kafka_python-0.54b1.tar.gz", hash = "sha256:8b3f18be44939a270ca55b8017c5f822b94bdc1372b59a49464b990c715d0ba4", size = 10535, upload-time = "2025-05-16T19:03:49.198Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/88/9998fac3940d818100f0b3b1b67992481df233516d4d0a14fce43d6dcbc8/opentelemetry_instrumentation_kafka_python-0.54b1-py3-none-any.whl", hash = "sha256:ab53ed8af3281a337feb5c1fa01059d5af99ec7aa84f2b360627a20fed385ab7", size = 11502, upload-time = "2025-05-16T19:02:48.012Z" },
]
[[package]]
name = "opentelemetry-instrumentation-psycopg"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-instrumentation-dbapi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/60/e882cfac324a7f9e51579a49401675f70b7bf96f7c9a3fc924e4062fdc11/opentelemetry_instrumentation_psycopg-0.54b1.tar.gz", hash = "sha256:e76f20fbde6e7a9302cbb5d3c6868c7dfb8284ce5800b32ab30635c8b51bb2db", size = 10501, upload-time = "2025-05-16T19:03:52.755Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/0b/78a8008b779a7f23f1c72642b73f6c7e63d8d970472c1cd22006e9dd539a/opentelemetry_instrumentation_psycopg-0.54b1-py3-none-any.whl", hash = "sha256:2484ed7932be0a12c740afd24a5c9c7ee4ceb1cd3622f5240a1218c1dcf0cad3", size = 11040, upload-time = "2025-05-16T19:02:53.427Z" },
]
[[package]]
name = "opentelemetry-instrumentation-redis"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/01/fad85231c3518bf6349a7ef483ef06a27100da8d1b7531dec9d8d09b94d8/opentelemetry_instrumentation_redis-0.54b1.tar.gz", hash = "sha256:89024c4752147d528e8c51fff0034193e628da339848cda78afe0cf4eb0c7ccb", size = 13908, upload-time = "2025-05-16T19:03:58.876Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/c1/78f18965f16e34a8fecc5b10c52aca1243e75a512a0a0320556a69583f36/opentelemetry_instrumentation_redis-0.54b1-py3-none-any.whl", hash = "sha256:e98992bd38e93081158f9947a1a8eea51d96e8bfe5054894a5b8d1d82117c0c8", size = 14924, upload-time = "2025-05-16T19:03:01.07Z" },
]
[[package]]
name = "opentelemetry-instrumentation-wsgi"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/0f/442eba02bd277fae2f5eb3ac5f8dd5f8cc52ddbe080506748871b91a63ab/opentelemetry_instrumentation_wsgi-0.54b1.tar.gz", hash = "sha256:261ad737e0058812aaae6bb7d6e0fa7344de62464c5df30c82bea180e735b903", size = 18244, upload-time = "2025-05-16T19:04:08.448Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/2f/075156d123e589d6728cc4c1a43d0335fa16e8f4a9f723a4af9267d91169/opentelemetry_instrumentation_wsgi-0.54b1-py3-none-any.whl", hash = "sha256:6d99dca32ce232251cd321bf86e8c9d0a60c5f088bcbe5ad55d12a2006fe056e", size = 14378, upload-time = "2025-05-16T19:03:15.074Z" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/dc/791f3d60a1ad8235930de23eea735ae1084be1c6f96fdadf38710662a7e5/opentelemetry_proto-1.33.1.tar.gz", hash = "sha256:9627b0a5c90753bf3920c398908307063e4458b287bb890e5c1d6fa11ad50b68", size = 34363, upload-time = "2025-05-16T18:52:52.141Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/29/48609f4c875c2b6c80930073c82dd1cafd36b6782244c01394007b528960/opentelemetry_proto-1.33.1-py3-none-any.whl", hash = "sha256:243d285d9f29663fc7ea91a7171fcc1ccbbfff43b48df0774fd64a37d98eda70", size = 55854, upload-time = "2025-05-16T18:52:36.269Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/67/12/909b98a7d9b110cce4b28d49b2e311797cffdce180371f35eba13a72dd00/opentelemetry_sdk-1.33.1.tar.gz", hash = "sha256:85b9fcf7c3d23506fbc9692fd210b8b025a1920535feec50bd54ce203d57a531", size = 161885, upload-time = "2025-05-16T18:52:52.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/8e/ae2d0742041e0bd7fe0d2dcc5e7cce51dcf7d3961a26072d5b43cc8fa2a7/opentelemetry_sdk-1.33.1-py3-none-any.whl", hash = "sha256:19ea73d9a01be29cacaa5d6c8ce0adc0b7f7b4d58cc52f923e4413609f670112", size = 118950, upload-time = "2025-05-16T18:52:37.297Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ name = "opentelemetry-api" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/2c/d7990fc1ffc82889d466e7cd680788ace44a26789809924813b164344393/opentelemetry_semantic_conventions-0.54b1.tar.gz", hash = "sha256:d1cecedae15d19bdaafca1e56b29a66aa286f50b5d08f036a145c7f3e9ef9cee", size = 118642, upload-time = "2025-05-16T18:52:53.962Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/80/08b1698c52ff76d96ba440bf15edc2f4bc0a279868778928e947c1004bdd/opentelemetry_semantic_conventions-0.54b1-py3-none-any.whl", hash = "sha256:29dab644a7e435b58d3a3918b58c333c92686236b30f7891d5e51f02933ca60d", size = 194938, upload-time = "2025-05-16T18:52:38.796Z" },
]
[[package]]
name = "opentelemetry-util-http"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/9f/1d8a1d1f34b9f62f2b940b388bf07b8167a8067e70870055bd05db354e5c/opentelemetry_util_http-0.54b1.tar.gz", hash = "sha256:f0b66868c19fbaf9c9d4e11f4a7599fa15d5ea50b884967a26ccd9d72c7c9d15", size = 8044, upload-time = "2025-05-16T19:04:10.79Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/ef/c5aa08abca6894792beed4c0405e85205b35b8e73d653571c9ff13a8e34e/opentelemetry_util_http-0.54b1-py3-none-any.whl", hash = "sha256:b1c91883f980344a1c3c486cffd47ae5c9c1dd7323f9cbe9fdb7cadb401c87c9", size = 7301, upload-time = "2025-05-16T19:03:18.18Z" },
]
[[package]]
name = "orjson"
version = "3.10.15"
@@ -3301,6 +3550,14 @@ dependencies = [
{ name = "numpy" },
{ name = "openai" },
{ name = "openpyxl" },
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-instrumentation-aiokafka" },
{ name = "opentelemetry-instrumentation-asgi" },
{ name = "opentelemetry-instrumentation-django" },
{ name = "opentelemetry-instrumentation-kafka-python" },
{ name = "opentelemetry-instrumentation-psycopg" },
{ name = "opentelemetry-instrumentation-redis" },
{ name = "opentelemetry-sdk" },
{ name = "orjson" },
{ name = "pandas" },
{ name = "paramiko" },
@@ -3501,6 +3758,14 @@ requires-dist = [
{ name = "numpy", specifier = "==1.26.4" },
{ name = "openai", specifier = "==1.77.0" },
{ name = "openpyxl", specifier = "==3.1.2" },
{ name = "opentelemetry-exporter-otlp-proto-grpc", specifier = "==1.33.1" },
{ name = "opentelemetry-instrumentation-aiokafka", specifier = "==0.54b1" },
{ name = "opentelemetry-instrumentation-asgi", specifier = "==0.54b1" },
{ name = "opentelemetry-instrumentation-django", specifier = "==0.54b1" },
{ name = "opentelemetry-instrumentation-kafka-python", specifier = "==0.54b1" },
{ name = "opentelemetry-instrumentation-psycopg", specifier = "==0.54b1" },
{ name = "opentelemetry-instrumentation-redis", specifier = "==0.54b1" },
{ name = "opentelemetry-sdk", specifier = "==1.33.1" },
{ name = "orjson", specifier = "==3.10.15" },
{ name = "pandas", specifier = "==2.2.0" },
{ name = "paramiko", specifier = "==3.4.0" },
@@ -6052,6 +6317,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/57/49/1091bd708f8892dc2ed5155bdf71ff51fcde75df137d65ac53f5d7f4fa25/zeep-4.2.1-py3-none-any.whl", hash = "sha256:6754feb4c34a4b6d65fbc359252bf6654dcce3937bf1d95aae4402a60a8f5939", size = 101212, upload-time = "2022-11-20T20:37:26.349Z" },
]
[[package]]
name = "zipp"
version = "3.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545, upload-time = "2024-11-10T15:05:20.202Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630, upload-time = "2024-11-10T15:05:19.275Z" },
]
[[package]]
name = "zstandard"
version = "0.23.0"