diff --git a/docs/integrations/http-clients/urllib3.md b/docs/integrations/http-clients/urllib3.md new file mode 100644 index 000000000..339e0e091 --- /dev/null +++ b/docs/integrations/http-clients/urllib3.md @@ -0,0 +1,36 @@ +--- +title: "Logfire urllib3 Integration: Setup Guide" +description: Learn how to use the logfire.instrument_urllib3() method to instrument urllib3 with Logfire. +integration: otel +--- +# urllib3 + +The [`logfire.instrument_urllib3()`][logfire.Logfire.instrument_urllib3] method can be used to +instrument [`urllib3`][urllib3] with **Logfire**. + +## Installation + +Install `logfire` with the `urllib3` extra: + +{{ install_logfire(extras=['urllib3']) }} + +## Usage + +```py title="main.py" skip-run="true" skip-reason="external-connection" +import urllib3 + +import logfire + +logfire.configure() +logfire.instrument_urllib3() + +http = urllib3.PoolManager() +http.request('GET', 'https://httpbin.org/get') +``` + +[`logfire.instrument_urllib3()`][logfire.Logfire.instrument_urllib3] uses the +**OpenTelemetry urllib3 Instrumentation** package, +which you can find more information about [here][opentelemetry-urllib3]. + +[opentelemetry-urllib3]: https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/urllib3/urllib3.html +[urllib3]: https://urllib3.readthedocs.io/ diff --git a/docs/integrations/index.md b/docs/integrations/index.md index 868ae2b23..627bf03b8 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -32,7 +32,7 @@ If a package you are using is not listed in this documentation, please let us kn - _LLM Clients and AI Frameworks_: Pydantic AI, OpenAI, Anthropic, LangChain, LlamaIndex, Mirascope, LiteLLM, Magentic - _Web Frameworks_: FastAPI, Django, Flask, Starlette, AIOHTTP, ASGI, WSGI - _Database Clients_: Psycopg, SQLAlchemy, Asyncpg, PyMongo, MySQL, SQLite3, Redis, BigQuery -- _HTTP Clients_: HTTPX, Requests, AIOHTTP +- _HTTP Clients_: HTTPX, Requests, urllib3, AIOHTTP - _Task Queues and Schedulers_: Airflow, FastStream, Celery - _Logging Libraries_: Standard Library Logging, Loguru, Structlog - _Testing_: Pytest @@ -77,6 +77,7 @@ The below table lists these integrations and any corresponding `logfire.instrume | [Stripe](stripe.md) | Payment Gateway | N/A (requires other instrumentations) | | [Structlog](structlog.md) | Logging | See documentation | | [System Metrics](system-metrics.md) | System Metrics | [`logfire.instrument_system_metrics()`][logfire.Logfire.instrument_system_metrics] | +| [urllib3](http-clients/urllib3.md) | HTTP Client | [`logfire.instrument_urllib3()`][logfire.Logfire.instrument_urllib3] | | [WSGI](web-frameworks/wsgi.md) | Web Framework Interface | [`logfire.instrument_wsgi()`][logfire.Logfire.instrument_wsgi] | If you are using Logfire with a web application, we also recommend reviewing diff --git a/logfire-api/logfire_api/__init__.py b/logfire-api/logfire_api/__init__.py index ab431b5ea..6eb8f02ff 100644 --- a/logfire-api/logfire_api/__init__.py +++ b/logfire-api/logfire_api/__init__.py @@ -172,6 +172,8 @@ def instrument_surrealdb(self, *args, **kwargs) -> None: ... def instrument_requests(self, *args, **kwargs) -> None: ... + def instrument_urllib3(self, *args, **kwargs) -> None: ... + def instrument_httpx(self, *args, **kwargs) -> None: ... def instrument_asyncpg(self, *args, **kwargs) -> None: ... @@ -193,6 +195,10 @@ def instrument_litellm(self, *args, **kwargs) -> None: ... def instrument_dspy(self, *args, **kwargs) -> None: ... + def instrument_celery(self, *args, **kwargs) -> None: ... + + def instrument_mysql(self, *args, **kwargs): ... + def instrument_aiohttp_client(self, *args, **kwargs) -> None: ... def instrument_aiohttp_server(self, *args, **kwargs) -> None: ... @@ -238,6 +244,7 @@ def shutdown(self, *args, **kwargs) -> None: ... instrument_celery = DEFAULT_LOGFIRE_INSTANCE.instrument_celery instrument_httpx = DEFAULT_LOGFIRE_INSTANCE.instrument_httpx instrument_requests = DEFAULT_LOGFIRE_INSTANCE.instrument_requests + instrument_urllib3 = DEFAULT_LOGFIRE_INSTANCE.instrument_urllib3 instrument_surrealdb = DEFAULT_LOGFIRE_INSTANCE.instrument_surrealdb instrument_psycopg = DEFAULT_LOGFIRE_INSTANCE.instrument_psycopg instrument_django = DEFAULT_LOGFIRE_INSTANCE.instrument_django diff --git a/logfire-api/logfire_api/__init__.pyi b/logfire-api/logfire_api/__init__.pyi index 27cfc4657..b1f84645f 100644 --- a/logfire-api/logfire_api/__init__.pyi +++ b/logfire-api/logfire_api/__init__.pyi @@ -16,7 +16,7 @@ from logfire.propagate import attach_context as attach_context, get_context as g from logfire.sampling import SamplingOptions as SamplingOptions from typing import Any -__all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'AdvancedOptions', 'ConsoleOptions', 'CodeSource', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'warning', 'error', 'exception', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_asgi', 'instrument_wsgi', 'instrument_pydantic', 'instrument_pydantic_ai', 'instrument_fastapi', 'instrument_openai', 'instrument_openai_agents', 'instrument_anthropic', 'instrument_google_genai', 'instrument_litellm', 'instrument_dspy', 'instrument_print', 'instrument_asyncpg', 'instrument_httpx', 'instrument_celery', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_aiohttp_server', 'instrument_sqlalchemy', 'instrument_sqlite3', 'instrument_aws_lambda', 'instrument_redis', 'instrument_pymongo', 'instrument_mysql', 'instrument_surrealdb', 'instrument_system_metrics', 'instrument_mcp', 'AutoTraceModule', 'with_tags', 'with_settings', 'suppress_scopes', 'shutdown', 'no_auto_trace', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'add_non_user_code_prefix', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'loguru_handler', 'SamplingOptions', 'MetricsOptions', 'VariablesOptions', 'LocalVariablesOptions', 'variables', 'var', 'variables_clear', 'variables_get', 'variables_push', 'variables_push_types', 'variables_validate', 'variables_push_config', 'variables_pull_config', 'variables_build_config', 'logfire_info', 'get_baggage', 'set_baggage', 'get_context', 'attach_context'] +__all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'AdvancedOptions', 'ConsoleOptions', 'CodeSource', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'warning', 'error', 'exception', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_asgi', 'instrument_wsgi', 'instrument_pydantic', 'instrument_pydantic_ai', 'instrument_fastapi', 'instrument_openai', 'instrument_openai_agents', 'instrument_anthropic', 'instrument_google_genai', 'instrument_litellm', 'instrument_dspy', 'instrument_print', 'instrument_asyncpg', 'instrument_httpx', 'instrument_celery', 'instrument_requests', 'instrument_urllib3', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_aiohttp_server', 'instrument_sqlalchemy', 'instrument_sqlite3', 'instrument_aws_lambda', 'instrument_redis', 'instrument_pymongo', 'instrument_mysql', 'instrument_surrealdb', 'instrument_system_metrics', 'instrument_mcp', 'AutoTraceModule', 'with_tags', 'with_settings', 'suppress_scopes', 'shutdown', 'no_auto_trace', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'add_non_user_code_prefix', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'loguru_handler', 'SamplingOptions', 'MetricsOptions', 'VariablesOptions', 'LocalVariablesOptions', 'variables', 'var', 'variables_clear', 'variables_get', 'variables_push', 'variables_push_types', 'variables_validate', 'variables_push_config', 'variables_pull_config', 'variables_build_config', 'logfire_info', 'get_baggage', 'set_baggage', 'get_context', 'attach_context'] DEFAULT_LOGFIRE_INSTANCE = Logfire() span = DEFAULT_LOGFIRE_INSTANCE.span @@ -40,6 +40,7 @@ instrument_asyncpg = DEFAULT_LOGFIRE_INSTANCE.instrument_asyncpg instrument_httpx = DEFAULT_LOGFIRE_INSTANCE.instrument_httpx instrument_celery = DEFAULT_LOGFIRE_INSTANCE.instrument_celery instrument_requests = DEFAULT_LOGFIRE_INSTANCE.instrument_requests +instrument_urllib3 = DEFAULT_LOGFIRE_INSTANCE.instrument_urllib3 instrument_psycopg = DEFAULT_LOGFIRE_INSTANCE.instrument_psycopg instrument_django = DEFAULT_LOGFIRE_INSTANCE.instrument_django instrument_flask = DEFAULT_LOGFIRE_INSTANCE.instrument_flask diff --git a/logfire-api/logfire_api/_internal/main.pyi b/logfire-api/logfire_api/_internal/main.pyi index 740a0aac5..6ca76fa18 100644 --- a/logfire-api/logfire_api/_internal/main.pyi +++ b/logfire-api/logfire_api/_internal/main.pyi @@ -5,6 +5,9 @@ import opentelemetry.trace as trace_api import pydantic_ai import pydantic_ai.models import requests +import urllib3.connectionpool +import urllib3.response +from opentelemetry.instrumentation.urllib3 import RequestInfo as Urllib3RequestInfo from . import async_ as async_ from ..integrations.aiohttp_client import RequestHook as AiohttpClientRequestHook, ResponseHook as AiohttpClientResponseHook from ..integrations.flask import CommenterOptions as FlaskCommenterOptions, RequestHook as FlaskRequestHook, ResponseHook as FlaskResponseHook @@ -738,6 +741,16 @@ class Logfire: response_hook: A function called right before a span is finished for the response. **kwargs: Additional keyword arguments to pass to the OpenTelemetry `instrument` methods, for future compatibility. """ + def instrument_urllib3(self, excluded_urls: str | None = None, request_hook: Callable[[Span, urllib3.connectionpool.HTTPConnectionPool, Urllib3RequestInfo], None] | None = None, response_hook: Callable[[Span, urllib3.connectionpool.HTTPConnectionPool, urllib3.response.HTTPResponse], None] | None = None, url_filter: Callable[[str], str] | None = None, **kwargs: Any) -> None: + """Instrument the `urllib3` module so that spans are automatically created for each request. + + Args: + excluded_urls: A string containing a comma-delimited list of regexes used to exclude URLs from tracking. + request_hook: A function called right after a span is created for a request. + response_hook: A function called right before a span is finished for the response. + url_filter: A callback to process the requested URL prior to adding it as a span attribute. + **kwargs: Additional keyword arguments to pass to the OpenTelemetry `instrument` methods, for future compatibility. + """ @overload def instrument_psycopg(self, conn_or_module: PsycopgConnection | Psycopg2Connection, **kwargs: Any) -> None: ... @overload diff --git a/logfire/__init__.py b/logfire/__init__.py index 8c0d01e02..ea66364b2 100644 --- a/logfire/__init__.py +++ b/logfire/__init__.py @@ -53,6 +53,7 @@ instrument_httpx = DEFAULT_LOGFIRE_INSTANCE.instrument_httpx instrument_celery = DEFAULT_LOGFIRE_INSTANCE.instrument_celery instrument_requests = DEFAULT_LOGFIRE_INSTANCE.instrument_requests +instrument_urllib3 = DEFAULT_LOGFIRE_INSTANCE.instrument_urllib3 instrument_psycopg = DEFAULT_LOGFIRE_INSTANCE.instrument_psycopg instrument_django = DEFAULT_LOGFIRE_INSTANCE.instrument_django instrument_flask = DEFAULT_LOGFIRE_INSTANCE.instrument_flask @@ -160,6 +161,7 @@ def loguru_handler() -> Any: 'instrument_httpx', 'instrument_celery', 'instrument_requests', + 'instrument_urllib3', 'instrument_psycopg', 'instrument_django', 'instrument_flask', diff --git a/logfire/_internal/integrations/urllib3.py b/logfire/_internal/integrations/urllib3.py new file mode 100644 index 000000000..e60043123 --- /dev/null +++ b/logfire/_internal/integrations/urllib3.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from typing import Any, Callable + +import urllib3.connectionpool +import urllib3.response +from opentelemetry.sdk.trace import Span + +try: + from opentelemetry.instrumentation.urllib3 import RequestInfo, URLLib3Instrumentor +except ImportError: + raise RuntimeError( + '`logfire.instrument_urllib3()` requires the `opentelemetry-instrumentation-urllib3` package.\n' + 'You can install this with:\n' + " pip install 'logfire[urllib3]'" + ) + + +def instrument_urllib3( + excluded_urls: str | None = None, + request_hook: Callable[[Span, urllib3.connectionpool.HTTPConnectionPool, RequestInfo], None] | None = None, + response_hook: Callable[[Span, urllib3.connectionpool.HTTPConnectionPool, urllib3.response.HTTPResponse], None] + | None = None, + url_filter: Callable[[str], str] | None = None, + **kwargs: Any, +) -> None: + """Instrument the `urllib3` module so that spans are automatically created for each request. + + See the `Logfire.instrument_urllib3` method for details. + """ + URLLib3Instrumentor().instrument( + excluded_urls=excluded_urls, + request_hook=request_hook, + response_hook=response_hook, + url_filter=url_filter, + **kwargs, + ) diff --git a/logfire/_internal/main.py b/logfire/_internal/main.py index 472910163..3b7880f7c 100644 --- a/logfire/_internal/main.py +++ b/logfire/_internal/main.py @@ -79,10 +79,13 @@ import openai import pydantic_ai.models import requests + import urllib3.connectionpool + import urllib3.response from django.http import HttpRequest, HttpResponse from fastapi import FastAPI from flask.app import Flask from opentelemetry.instrumentation.asgi.types import ClientRequestHook, ClientResponseHook, ServerRequestHook + from opentelemetry.instrumentation.urllib3 import RequestInfo as Urllib3RequestInfo from opentelemetry.metrics import _Gauge as Gauge from pymongo.monitoring import CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent from sqlalchemy import Engine @@ -1680,6 +1683,40 @@ def instrument_requests( }, ) + def instrument_urllib3( + self, + excluded_urls: str | None = None, + request_hook: Callable[[Span, urllib3.connectionpool.HTTPConnectionPool, Urllib3RequestInfo], None] + | None = None, + response_hook: Callable[[Span, urllib3.connectionpool.HTTPConnectionPool, urllib3.response.HTTPResponse], None] + | None = None, + url_filter: Callable[[str], str] | None = None, + **kwargs: Any, + ) -> None: + """Instrument the `urllib3` module so that spans are automatically created for each request. + + Args: + excluded_urls: A string containing a comma-delimited list of regexes used to exclude URLs from tracking. + request_hook: A function called right after a span is created for a request. + response_hook: A function called right before a span is finished for the response. + url_filter: A callback to process the requested URL prior to adding it as a span attribute. + **kwargs: Additional keyword arguments to pass to the OpenTelemetry `instrument` methods, for future compatibility. + """ + from .integrations.urllib3 import instrument_urllib3 + + self._warn_if_not_initialized_for_instrumentation() + return instrument_urllib3( + excluded_urls=excluded_urls, + request_hook=request_hook, + response_hook=response_hook, + url_filter=url_filter, + **{ + 'tracer_provider': self._config.get_tracer_provider(), + 'meter_provider': self._config.get_meter_provider(), + **kwargs, + }, + ) + @overload def instrument_psycopg(self, conn_or_module: PsycopgConnection | Psycopg2Connection, **kwargs: Any) -> None: ... diff --git a/mkdocs.yml b/mkdocs.yml index b3dc6c94b..abbdb8862 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -166,6 +166,7 @@ nav: - HTTP Clients: - HTTPX: integrations/http-clients/httpx.md - Requests: integrations/http-clients/requests.md + - urllib3: integrations/http-clients/urllib3.md - AIOHTTP: integrations/http-clients/aiohttp.md - Task Queues & Pipelines: - Airflow: integrations/event-streams/airflow.md diff --git a/pyproject.toml b/pyproject.toml index 2de689058..7c2f24777 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ psycopg2 = ["opentelemetry-instrumentation-psycopg2 >= 0.42b0", "packaging"] pymongo = ["opentelemetry-instrumentation-pymongo >= 0.42b0"] redis = ["opentelemetry-instrumentation-redis >= 0.42b0"] requests = ["opentelemetry-instrumentation-requests >= 0.42b0"] +urllib3 = ["opentelemetry-instrumentation-urllib3 >= 0.42b0"] mysql = ["opentelemetry-instrumentation-mysql >= 0.42b0"] sqlite3 = ["opentelemetry-instrumentation-sqlite3 >= 0.42b0"] aws-lambda = ["opentelemetry-instrumentation-aws-lambda >= 0.42b0"] @@ -131,6 +132,7 @@ dev = [ "opentelemetry-instrumentation-django>=0.42b0", "opentelemetry-instrumentation-httpx>=0.42b0", "opentelemetry-instrumentation-requests>=0.42b0", + "opentelemetry-instrumentation-urllib3>=0.42b0", "opentelemetry-instrumentation-sqlalchemy>=0.42b0", "opentelemetry-instrumentation-system-metrics>=0.42b0", "opentelemetry-instrumentation-asyncpg>=0.42b0", diff --git a/tests/otel_integrations/cassettes/test_urllib3/test_urllib3_instrumentation.yaml b/tests/otel_integrations/cassettes/test_urllib3/test_urllib3_instrumentation.yaml new file mode 100644 index 000000000..36728a8fd --- /dev/null +++ b/tests/otel_integrations/cassettes/test_urllib3/test_urllib3_instrumentation.yaml @@ -0,0 +1,26 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Host: + - example.org:8080 + User-Agent: + - python-urllib3/2.4.0 + method: GET + uri: https://example.org:8080/foo + response: + body: + string: '' + headers: + Content-Length: + - '0' + Content-Type: + - text/html + status: + code: 200 + message: OK +version: 1 diff --git a/tests/otel_integrations/test_urllib3.py b/tests/otel_integrations/test_urllib3.py new file mode 100644 index 000000000..c72f1429f --- /dev/null +++ b/tests/otel_integrations/test_urllib3.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import importlib +from typing import Any +from unittest import mock + +import pytest +import urllib3 +from dirty_equals import IsFloat, IsNumeric +from inline_snapshot import snapshot +from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor + +import logfire +import logfire._internal.integrations.urllib3 +from logfire.testing import TestExporter + +_COMMON_OLD_METRIC_ATTRS = { + 'http.host': 'example.org', + 'http.method': 'GET', + 'http.scheme': 'https', + 'http.status_code': 200, + 'net.peer.name': 'example.org', + 'net.peer.port': 8080, +} + +_COMMON_NEW_METRIC_ATTRS = { + 'http.request.method': 'GET', + 'http.response.status_code': 200, + 'server.address': 'example.org', + 'server.port': 8080, +} + + +def _expected_metrics() -> dict[str, Any]: + return { + 'http.client.duration': { + 'details': [{'attributes': _COMMON_OLD_METRIC_ATTRS, 'total': IsNumeric()}], + 'total': IsNumeric(), + }, + 'http.client.request.duration': { + 'details': [{'attributes': _COMMON_NEW_METRIC_ATTRS, 'total': IsFloat()}], + 'total': IsFloat(), + }, + 'http.client.request.size': { + 'details': [{'attributes': _COMMON_OLD_METRIC_ATTRS, 'total': IsNumeric()}], + 'total': IsNumeric(), + }, + 'http.client.request.body.size': { + 'details': [{'attributes': _COMMON_NEW_METRIC_ATTRS, 'total': IsNumeric()}], + 'total': IsNumeric(), + }, + 'http.client.response.size': { + 'details': [{'attributes': _COMMON_OLD_METRIC_ATTRS, 'total': IsNumeric()}], + 'total': IsNumeric(), + }, + 'http.client.response.body.size': { + 'details': [{'attributes': _COMMON_NEW_METRIC_ATTRS, 'total': IsNumeric()}], + 'total': IsNumeric(), + }, + } + + +@pytest.fixture(autouse=True) +def uninstrument_urllib3(): + logfire.instrument_urllib3() + yield + URLLib3Instrumentor().uninstrument() + + +@pytest.mark.anyio +@pytest.mark.vcr() +async def test_urllib3_instrumentation(exporter: TestExporter): + with logfire.span('test span'): + http = urllib3.PoolManager() + http.request('GET', 'https://example.org:8080/foo') + + assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( + [ + { + 'name': 'GET', + 'context': {'trace_id': 1, 'span_id': 3, 'is_remote': False}, + 'parent': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, + 'start_time': 2000000000, + 'end_time': 3000000000, + 'attributes': { + 'http.method': 'GET', + 'http.request.method': 'GET', + 'http.url': 'https://example.org:8080/foo', + 'url.full': 'https://example.org:8080/foo', + 'logfire.span_type': 'span', + 'logfire.msg': 'GET example.org/foo', + 'http.status_code': 200, + 'http.response.status_code': 200, + 'logfire.metrics': _expected_metrics(), + 'http.target': '/foo', + }, + }, + { + 'name': 'test span', + 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, + 'parent': None, + 'start_time': 1000000000, + 'end_time': 4000000000, + 'attributes': { + 'code.filepath': 'test_urllib3.py', + 'code.lineno': 123, + 'code.function': 'test_urllib3_instrumentation', + 'logfire.msg_template': 'test span', + 'logfire.span_type': 'span', + 'logfire.msg': 'test span', + 'logfire.metrics': _expected_metrics(), + }, + }, + ] + ) + + +def test_missing_opentelemetry_dependency() -> None: + with mock.patch.dict('sys.modules', {'opentelemetry.instrumentation.urllib3': None}): + with pytest.raises(RuntimeError) as exc_info: + importlib.reload(logfire._internal.integrations.urllib3) + assert str(exc_info.value) == snapshot("""\ +`logfire.instrument_urllib3()` requires the `opentelemetry-instrumentation-urllib3` package. +You can install this with: + pip install 'logfire[urllib3]'\ +""") diff --git a/uv.lock b/uv.lock index 766efd1be..a8f5fc738 100644 --- a/uv.lock +++ b/uv.lock @@ -669,7 +669,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy'" }, + { name = "pycparser", version = "2.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'" }, { name = "pycparser", version = "3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } @@ -1531,7 +1531,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -3318,6 +3318,9 @@ starlette = [ system-metrics = [ { name = "opentelemetry-instrumentation-system-metrics" }, ] +urllib3 = [ + { name = "opentelemetry-instrumentation-urllib3" }, +] variables = [ { name = "pydantic" }, ] @@ -3394,6 +3397,7 @@ dev = [ { name = "opentelemetry-instrumentation-sqlite3" }, { name = "opentelemetry-instrumentation-starlette" }, { name = "opentelemetry-instrumentation-system-metrics" }, + { name = "opentelemetry-instrumentation-urllib3" }, { name = "opentelemetry-instrumentation-wsgi" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -3487,6 +3491,7 @@ requires-dist = [ { name = "opentelemetry-instrumentation-sqlite3", marker = "extra == 'sqlite3'", specifier = ">=0.42b0" }, { name = "opentelemetry-instrumentation-starlette", marker = "extra == 'starlette'", specifier = ">=0.42b0" }, { name = "opentelemetry-instrumentation-system-metrics", marker = "extra == 'system-metrics'", specifier = ">=0.42b0" }, + { name = "opentelemetry-instrumentation-urllib3", marker = "extra == 'urllib3'", specifier = ">=0.42b0" }, { name = "opentelemetry-instrumentation-wsgi", marker = "extra == 'wsgi'", specifier = ">=0.42b0" }, { name = "opentelemetry-sdk", specifier = ">=1.39.0,<1.40.0" }, { name = "packaging", marker = "extra == 'psycopg'" }, @@ -3499,7 +3504,7 @@ requires-dist = [ { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.1" }, { name = "typing-extensions", specifier = ">=4.1.0" }, ] -provides-extras = ["system-metrics", "asgi", "wsgi", "aiohttp", "aiohttp-client", "aiohttp-server", "celery", "django", "fastapi", "flask", "httpx", "starlette", "sqlalchemy", "asyncpg", "psycopg", "psycopg2", "pymongo", "redis", "requests", "mysql", "sqlite3", "aws-lambda", "google-genai", "litellm", "dspy", "datasets", "variables"] +provides-extras = ["system-metrics", "asgi", "wsgi", "aiohttp", "aiohttp-client", "aiohttp-server", "celery", "django", "fastapi", "flask", "httpx", "starlette", "sqlalchemy", "asyncpg", "psycopg", "psycopg2", "pymongo", "redis", "requests", "urllib3", "mysql", "sqlite3", "aws-lambda", "google-genai", "litellm", "dspy", "datasets", "variables"] [package.metadata.requires-dev] dev = [ @@ -3559,6 +3564,7 @@ dev = [ { name = "opentelemetry-instrumentation-sqlite3", specifier = ">=0.42b0" }, { name = "opentelemetry-instrumentation-starlette", specifier = ">=0.42b0" }, { name = "opentelemetry-instrumentation-system-metrics", specifier = ">=0.42b0" }, + { name = "opentelemetry-instrumentation-urllib3", specifier = ">=0.42b0" }, { name = "opentelemetry-instrumentation-wsgi", specifier = ">=0.42b0" }, { name = "pandas", specifier = ">=2.1.2" }, { name = "pip", specifier = ">=0" }, @@ -5178,6 +5184,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/a1/fef3de5fba4f80012af7be501e8d216048aa710929d9818e1d2cd3ddc854/opentelemetry_instrumentation_system_metrics-0.60b1-py3-none-any.whl", hash = "sha256:21fb040ed6cfabc8ca97c63548fd01689f7ec92c64bbc6cfd08f30489a336fc6", size = 13516, upload-time = "2025-12-11T13:36:27.579Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-urllib3" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/0c/090ab43417f37b2e2044310de219a8913f4377c75a9f19b2fcaaaeccf0ec/opentelemetry_instrumentation_urllib3-0.60b1.tar.gz", hash = "sha256:1f01cdde3be155ab181fc4cf3363457ff0901f417ac8a102712ee7b7539c9f39", size = 15790, upload-time = "2025-12-11T13:37:19.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/d3411ae68983a8e7ca7195dc0fc2333a4f83e75f6943a30e69ede4e5fe48/opentelemetry_instrumentation_urllib3-0.60b1-py3-none-any.whl", hash = "sha256:4f17b5d41b25cc1b318260ca32f5321afc65017e4be533b65cd804c52855fdf7", size = 13187, upload-time = "2025-12-11T13:36:32.265Z" }, +] + [[package]] name = "opentelemetry-instrumentation-wsgi" version = "0.60b1"