Repository: urllib3/urllib3

Issue:
urllib3 does not send http.client.connect audit events
### Subject

Despite being implemented in terms of `http.client`, urllib3 doesn't cause `http.client.connect` audit events to be emitted

### Steps to Reproduce

```python
import http.client
import sys

import urllib3


def _hook(event: str, args: tuple):
    if event == "http.client.connect":
        print(event, args)
    if event == 'socket.connect':
        print(event, args)


sys.addaudithook(_hook)

print('using http.client')
http.client.HTTPConnection('www.python.org').connect()
print()
print('using urllib3')
http = urllib3.PoolManager()
resp = http.request("GET", "https://www.python.org")
```

```
using http.client
http.client.connect (<http.client.HTTPConnection object at 0x7f1303983fd0>, 'www.python.org', 80)
socket.connect (<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('0.0.0.0', 0)>, ('199.232.36.223', 80))

using urllib3
socket.connect (<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('0.0.0.0', 0)>, ('199.232.36.223', 443))
```

This is because the urllib3 `HTTPConnection` object overrides `connect`, which is where the audit events are sent from.
https://github.com/python/cpython/blob/e87ada48a9e5d9d03f9759138869216df0d7383a/Lib/http/client.py#L954

### Expected Behavior

`http.client.connect` audit events should be emitted

### Actual Behavior

They aren't



Discussion:
If we don't use http.client but try to provide a similar API not a fully complete implementation, why must we do what only standard libraries do? 
My expectation that urllib3 would send this audit event is this sentence in the API docs:

> Based on http.client.HTTPConnection

To me, this implies that it is a superset of the functionality present in http.client.HTTPConnection.

Regardless of implementation details, it would be useful for urllib3 to send `http.client.connect` audit events - or less preferably, a different audit event.

> My expectation that urllib3 would send this audit event is this sentence in the API docs:
> 
> > Based on http.client.HTTPConnection
> 
> To me, this implies that it is a superset of the functionality present in http.client.HTTPConnection.
> 
> Regardless of implementation details, it would be useful for urllib3 to send `http.client.connect` audit events - or less preferably, a different audit event.
> 

If we're not using HTTPConnection.connect, why should it emit? Some things may be the same or overlap, but that isn't guaranteed to happen. Further, this library shouldn't be manufacturing events another library presently generates as that library may change them without notice. We also don't want to rely on the standard library's connect method.

I'd also argue that http.client is the namespace that's owned by the cpython core developers, no one should be reusing it. If we emitted anything to the audit events (which arguably isn't intended for libraries outside the stdlib) it would be under a namespace we ostensibly can claim to own 
> If we're not using HTTPConnection.connect

This is an implementation detail that doesn't excuse the inconsistency.
You are deriving behavior from `http.client.HTTPConnection` and then breaking the public contract it provides.

This is further evidenced by the fact that `urllib3` DOES cause `http.client.send` audits to be emitted.

```python
import http.client
import sys

import urllib3


def _hook(event: str, args: tuple):
    if event == "http.client.connect":
        print(event, args)
    if event == "http.client.send":
        print(event, args)
    if event == 'socket.connect':
        print(event, args)


sys.addaudithook(_hook)

print('using http.client')
con = http.client.HTTPConnection('www.python.org')
con.connect()
con.request('GET', '/')
print()
print('using urllib3')
http = urllib3.PoolManager()
resp = http.request("GET", "https://www.python.org")
```

```
using http.client
http.client.connect (<http.client.HTTPConnection object at 0x7fabc2dfffd0>, 'www.python.org', 80)
socket.connect (<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('0.0.0.0', 0)>, ('199.232.36.223', 80))
http.client.send (<http.client.HTTPConnection object at 0x7fabc2dfffd0>, b'GET / HTTP/1.1\r\nHost: www.python.org\r\nAccept-Encoding: identity\r\n\r\n')

using urllib3
socket.connect (<socket.socket fd=5, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('0.0.0.0', 0)>, ('199.232.36.223', 443))
http.client.send (<urllib3.connection.HTTPSConnection object at 0x7fabc24c2ce0>, b'GET / HTTP/1.1\r\nHost: www.python.org\r\nAccept-Encoding: identity\r\nUser-Agent: python-urllib3/1.26.9\r\n\r\n')
```

Is this behavior guaranteed? subject to breakage in the future if urllib3 decides to override the [`send` method](https://github.com/python/cpython/blob/e87ada48a9e5d9d03f9759138869216df0d7383a/Lib/http/client.py#L1010)?

> Further, this library shouldn't be manufacturing events another library presently generates as that library may change them without notice.

According to the [docs](https://docs.python.org/3/library/sys.html#sys.audit) "The number and types of arguments for a given event are considered a public and stable API and should not be modified between releases". `http.client.connect` is well defined and not subject to arbitrary change.

>which arguably isn't intended for libraries outside the stdlib

I can find no evidence of this anywhere in the documentation.

For some context, this is presenting real-world problems for an application in my day job.

We would like to have some stronger guarantees that after a certain point in the application lifecycle there are no connections being made to certain hosts within our organization.  We have implemented this using `http.client.connect` audit hooks.  Due to this bug, this only works some of the time - some requests are made using libraries implemented in urllib3, and some using `http.client`.
Thanks for bringing this to our attention, I hadn't thought of audithooks. We're in a bit of a tough spot as our use of http.client is historical and (if all goes to plan) something that we'd like to move away from in the future.

Here's what I'm thinking:

- Let's match the standard library audithooks in 1.26.x and have a test case that compares what is emitted using http.client directly to what we emit.
- Document that audithooks for http.client only fire for the HTTPConnection based on the stdlib http.client which in 2.0 and onwards isn't guaranteed to be the default.
- Investigate adding out oen audithook events that we can make better guarantees for?

How does that sound? Also @apmorton would you be interested in contributing work towards this if we decide to move forward?
That all sounds reasonable to me - I'd be happy to contribute work on this issue.

Resolving patch:
diff --git a/src/urllib3/connection.py b/src/urllib3/connection.py
--- a/src/urllib3/connection.py
+++ b/src/urllib3/connection.py
@@ -5,6 +5,7 @@
 import os
 import re
 import socket
+import sys
 import typing
 import warnings
 from http.client import HTTPConnection as _HTTPConnection
@@ -76,6 +77,8 @@ class BaseSSLError(BaseException):  # type: ignore[no-redef]
 
 _CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
 
+_HAS_SYS_AUDIT = hasattr(sys, "audit")
+
 
 class HTTPConnection(_HTTPConnection):
     """
@@ -216,6 +219,10 @@ def _new_conn(self) -> socket.socket:
                 self, f"Failed to establish a new connection: {e}"
             ) from e
 
+        # Audit hooks are only available in Python 3.8+
+        if _HAS_SYS_AUDIT:
+            sys.audit("http.client.connect", self, self.host, self.port)
+
         return sock
 
     def set_tunnel(


Test patch:
diff --git a/test/with_dummyserver/test_connection.py b/test/with_dummyserver/test_connection.py
--- a/test/with_dummyserver/test_connection.py
+++ b/test/with_dummyserver/test_connection.py
@@ -1,7 +1,9 @@
 from __future__ import annotations
 
+import sys
 import typing
 from http.client import ResponseNotReady
+from unittest import mock
 
 import pytest
 
@@ -33,6 +35,19 @@ def test_returns_urllib3_HTTPResponse(pool: HTTPConnectionPool) -> None:
     assert isinstance(response, HTTPResponse)
 
 
+@pytest.mark.skipif(not hasattr(sys, "audit"), reason="requires python 3.8+")
+@mock.patch("urllib3.connection.sys.audit")
+def test_audit_event(audit_mock: mock.Mock, pool: HTTPConnectionPool) -> None:
+    conn = pool._get_conn()
+    conn.request("GET", "/")
+    audit_mock.assert_any_call("http.client.connect", conn, conn.host, conn.port)
+    # Ensure the event is raised only once.
+    connect_events = [
+        call for call in audit_mock.mock_calls if call.args[0] == "http.client.connect"
+    ]
+    assert len(connect_events) == 1
+
+
 def test_does_not_release_conn(pool: HTTPConnectionPool) -> None:
     conn = pool._get_conn()