mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat: add django tracing (#32418)
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
17
bin/start
17
bin/start
@@ -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
|
||||
|
||||
@@ -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/"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
28
otel-collector-config.dev.yaml
Normal file
28
otel-collector-config.dev.yaml
Normal 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
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
150
posthog/otel_instrumentation.py
Normal file
150
posthog/otel_instrumentation.py
Normal 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",
|
||||
)
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
347
posthog/test/test_otel_instrumentation.py
Normal file
347
posthog/test/test_otel_instrumentation.py
Normal 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)
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
274
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user