diff --git a/docs/integrations/http-clients/urllib3.md b/docs/integrations/http-clients/urllib3.md new file mode 100644 index 000000000..14687db43 --- /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][opentelemetry-urllib3] library, which creates a span for each request made +with `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 38f01b88a..11fb960ec 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 diff --git a/logfire-api/logfire_api/__init__.py b/logfire-api/logfire_api/__init__.py index 132762844..d79b03528 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: ... @@ -249,6 +251,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 c9783a0c6..570a474dd 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', 'instrument_claude_agent_sdk', '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', 'template_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', 'url_from_eval', 'forward_export_request', 'forward_export_request_starlette'] +__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', 'instrument_claude_agent_sdk', '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', 'template_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', 'url_from_eval', 'forward_export_request', 'forward_export_request_starlette'] 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 269bce4d6..e2f17dc6b 100644 --- a/logfire-api/logfire_api/_internal/main.pyi +++ b/logfire-api/logfire_api/_internal/main.pyi @@ -5,6 +5,8 @@ import opentelemetry.trace as trace_api import pydantic_ai import pydantic_ai.models import requests +import urllib3.connectionpool +import urllib3.response from . import async_ as async_ from ..experimental.forwarding import ForwardExportRequestResponse as ForwardExportRequestResponse from ..integrations.aiohttp_client import RequestHook as AiohttpClientRequestHook, ResponseHook as AiohttpClientResponseHook @@ -43,6 +45,7 @@ from fastapi import FastAPI from flask.app import Flask from opentelemetry.context import Context as Context from opentelemetry.instrumentation.asgi.types import ClientRequestHook, ClientResponseHook, ServerRequestHook +from opentelemetry.instrumentation.urllib3 import RequestInfo as Urllib3RequestInfo from opentelemetry.metrics import CallbackT as CallbackT, Counter, Histogram, UpDownCounter, _Gauge as Gauge from opentelemetry.sdk.trace import ReadableSpan, Span from opentelemetry.trace import SpanContext, SpanKind @@ -773,6 +776,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 f50ca97df..8ffd36d35 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 @@ -165,6 +166,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..28edc8474 --- /dev/null +++ b/logfire/_internal/integrations/urllib3.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +from opentelemetry.sdk.trace import Span + +try: + from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor +except ImportError as _err: + raise RuntimeError( + '`logfire.instrument_urllib3()` requires the `opentelemetry-instrumentation-urllib3` package.\n' + 'You can install this with:\n' + " pip install 'logfire[urllib3]'" + ) from _err + +if TYPE_CHECKING: + import urllib3.connectionpool + import urllib3.response + from opentelemetry.instrumentation.urllib3 import RequestInfo + + +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 63aa8a91c..42195ba72 100644 --- a/logfire/_internal/main.py +++ b/logfire/_internal/main.py @@ -78,6 +78,8 @@ import openai import pydantic_ai.models import requests + import urllib3.connectionpool + import urllib3.response from anthropic.lib.bedrock import ( AnthropicBedrock as _AnthropicBedrock, AsyncAnthropicBedrock as _AsyncAnthropicBedrock, @@ -86,6 +88,7 @@ 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 pydantic_evals.reporting import EvaluationReport from pymongo.monitoring import CommandFailedEvent, CommandStartedEvent, CommandSucceededEvent @@ -1746,6 +1749,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 a5d125ffa..ae99999fa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -161,6 +161,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 7fb41ec16..8f36fd9a8 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..7a3b9b0fc --- /dev/null +++ b/tests/otel_integrations/cassettes/test_urllib3/test_urllib3_instrumentation.yaml @@ -0,0 +1,34 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Host: + - httpbin.org + User-Agent: + - python-urllib3/2.4.0 + method: GET + uri: https://httpbin.org:443/get + response: + body: + string: '{"args":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate","Host":"httpbin.org","User-Agent":"python-urllib3/2.4.0"},"origin":"10.0.0.1","url":"https://httpbin.org/get"}' + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Origin: + - '*' + Content-Length: + - '219' + Content-Type: + - application/json + Date: + - Fri, 17 Jan 2026 12:00:00 GMT + Server: + - gunicorn/19.9.0 + 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..417f23512 --- /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': 'httpbin.org', + 'http.method': 'GET', + 'http.scheme': 'https', + 'http.status_code': 200, + 'net.peer.name': 'httpbin.org', + 'net.peer.port': 443, +} + +_COMMON_NEW_METRIC_ATTRS = { + 'http.request.method': 'GET', + 'http.response.status_code': 200, + 'server.address': 'httpbin.org', + 'server.port': 443, +} + + +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://httpbin.org/get') + + 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://httpbin.org/get', + 'url.full': 'https://httpbin.org/get', + 'logfire.span_type': 'span', + 'logfire.msg': 'GET httpbin.org/get', + 'http.status_code': 200, + 'http.response.status_code': 200, + 'logfire.metrics': _expected_metrics(), + 'http.target': '/get', + }, + }, + { + '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 d566b34fe..ad5db64c8 100644 --- a/uv.lock +++ b/uv.lock @@ -1180,7 +1180,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 = [ @@ -2346,6 +2346,9 @@ starlette = [ system-metrics = [ { name = "opentelemetry-instrumentation-system-metrics" }, ] +urllib3 = [ + { name = "opentelemetry-instrumentation-urllib3" }, +] variables = [ { name = "pydantic" }, { name = "pydantic-handlebars" }, @@ -2414,6 +2417,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.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -2483,6 +2487,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.43.0" }, { name = "packaging", marker = "extra == 'psycopg'" }, @@ -2498,7 +2503,7 @@ requires-dist = [ { name = "typing-extensions", specifier = ">=4.1.0" }, { name = "uvicorn", marker = "extra == 'gateway'", specifier = ">=0.30.0" }, ] -provides-extras = ["gateway", "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 = ["gateway", "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 = [ @@ -2558,6 +2563,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" }, @@ -3652,6 +3658,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/37/b8bddfab16c0a36af1941ec041b5bc8fe3c3d802997b4e819037908a90d5/opentelemetry_instrumentation_system_metrics-0.63b1-py3-none-any.whl", hash = "sha256:995051f47876d79461aed8b7aa205d4584d90794ef864342cc748929c389bb42", size = 14006, upload-time = "2026-05-21T16:35:56.979Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-urllib3" +version = "0.63b1" +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/c9/b9/acc5bd0add608cad1abebe9d65c0916bf48c8957c2a6b0b3985ead79830e/opentelemetry_instrumentation_urllib3-0.63b1.tar.gz", hash = "sha256:c4358358f49b7dc42550cd6efbcfbfce3d178b8bf09acf46b62993f5f3ba4a9c", size = 18945, upload-time = "2026-05-21T16:36:53.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/92/a9fac2daa2e4d843d6330263d56b18f32a5de43573e6c869da439e84ddf2/opentelemetry_instrumentation_urllib3-0.63b1-py3-none-any.whl", hash = "sha256:241d0a819e614e479ba89d32470dddd94191deb0cb49fa525fa5585022caddbe", size = 13512, upload-time = "2026-05-21T16:36:02.849Z" }, +] + [[package]] name = "opentelemetry-instrumentation-wsgi" version = "0.63b1"