Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
90e2f45
feat: add urllib3 instrumentation integration
alloevil Jun 21, 2026
66f22d2
feat: add urllib3 instrumentation integration
alloevil Jun 21, 2026
95839b9
fix: use explicit exception chaining in ImportError handler
alloevil Jun 21, 2026
7e85446
chore: add opentelemetry-instrumentation-urllib3 to uv.lock
alloevil Jun 21, 2026
4514aee
fix: add missing blank line in logfire-api __init__.py stub
alloevil Jun 21, 2026
436e043
fix: fix import order in main.pyi type stubs
alloevil Jun 21, 2026
1f506a3
fix: fix indentation of instrument_urllib3 in logfire-api __init__.py
alloevil Jun 21, 2026
cd66ce5
fix: import Callable from collections.abc (UP035)
alloevil Jun 21, 2026
c269799
fix: fix indentation and Callable import (UP035)
alloevil Jun 21, 2026
621b07e
fix: fix import order in main.pyi type stubs
alloevil Jun 21, 2026
c676a16
fix: remove urllib3 from dev deps, revert uv.lock
alloevil Jun 21, 2026
3f89e30
fix: restore dev dep and revert uv.lock, add CI lock step
alloevil Jun 21, 2026
e838e8a
fix: remove urllib3 from dev deps (use importorskip in tests)
alloevil Jun 21, 2026
32978ad
fix: revert pyproject.toml and uv.lock to upstream
alloevil Jun 21, 2026
c5f74aa
fix: revert pyproject.toml and uv.lock to exact upstream
alloevil Jun 21, 2026
6027cbd
fix: revert pyproject.toml and uv.lock to upstream (blob refs)
alloevil Jun 21, 2026
7b312ed
fix: add urllib3 to pyproject.toml deps + move imports to TYPE_CHECKING
alloevil Jun 22, 2026
a0258ff
chore: update uv.lock with opentelemetry-instrumentation-urllib3
alloevil Jun 22, 2026
3a7694d
fix: move RequestInfo to TYPE_CHECKING block for pyright strict mode
alloevil Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/integrations/http-clients/urllib3.md
Original file line number Diff line number Diff line change
@@ -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/
2 changes: 1 addition & 1 deletion docs/integrations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Integrations doc table missing urllib3 row

The bullet list at docs/integrations/index.md:35 was updated to include urllib3, but the detailed integrations table (lines 43–80) was not updated with a corresponding row for urllib3. Every other integration listed in the bullet points has a matching row in the table (e.g., Requests at line 72, HTTPX at line 59). This is a documentation completeness gap rather than a code bug.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- _Task Queues and Schedulers_: Airflow, FastStream, Celery
- _Logging Libraries_: Standard Library Logging, Loguru, Structlog
- _Testing_: Pytest
Expand Down
3 changes: 3 additions & 0 deletions logfire-api/logfire_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion logfire-api/logfire_api/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions logfire-api/logfire_api/_internal/main.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions logfire/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -165,6 +166,7 @@ def loguru_handler() -> Any:
'instrument_httpx',
'instrument_celery',
'instrument_requests',
'instrument_urllib3',
'instrument_psycopg',
'instrument_django',
'instrument_flask',
Expand Down
41 changes: 41 additions & 0 deletions logfire/_internal/integrations/urllib3.py
Original file line number Diff line number Diff line change
@@ -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,
)
37 changes: 37 additions & 0 deletions logfire/_internal/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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: ...

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading