Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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** 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/
3 changes: 2 additions & 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
- _Task Queues and Schedulers_: Airflow, FastStream, Celery
- _Logging Libraries_: Standard Library Logging, Loguru, Structlog
- _Testing_: Pytest
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 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 All @@ -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: ...
Expand Down Expand Up @@ -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
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', '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
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,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
Expand Down Expand Up @@ -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
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 @@ -160,6 +161,7 @@ def loguru_handler() -> Any:
'instrument_httpx',
'instrument_celery',
'instrument_requests',
'instrument_urllib3',
'instrument_psycopg',
'instrument_django',
'instrument_flask',
Expand Down
37 changes: 37 additions & 0 deletions logfire/_internal/integrations/urllib3.py
Original file line number Diff line number Diff line change
@@ -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,
)
37 changes: 37 additions & 0 deletions logfire/_internal/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: ...

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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,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
Copy link
Contributor

Choose a reason for hiding this comment

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

was this cassette written by AI instead of generated automatically by pytest? https://example.org:8080/foo doesn't return 200

response:
body:
string: ''
headers:
Content-Length:
- '0'
Content-Type:
- text/html
status:
code: 200
message: OK
version: 1
Loading
Loading