Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ssl object logging to the profiler #93014

Closed
wants to merge 2 commits into from
Closed
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
69 changes: 69 additions & 0 deletions homeassistant/components/profiler/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""The profiler integration."""
import asyncio
from asyncio.sslproto import SSLProtocol, _SSLProtocolTransport
from contextlib import suppress
from datetime import timedelta
from functools import _lru_cache_wrapper
import logging
import reprlib
import ssl
import sys
import threading
import time
Expand Down Expand Up @@ -35,6 +37,7 @@
SERVICE_LRU_STATS = "lru_stats"
SERVICE_LOG_THREAD_FRAMES = "log_thread_frames"
SERVICE_LOG_EVENT_LOOP_SCHEDULED = "log_event_loop_scheduled"
SERVICE_LOG_SSL = "log_ssl"

_LRU_CACHE_WRAPPER_OBJECT = _lru_cache_wrapper.__name__
_SQLALCHEMY_LRU_OBJECT = "LRUCache"
Expand All @@ -58,6 +61,7 @@
SERVICE_LRU_STATS,
SERVICE_LOG_THREAD_FRAMES,
SERVICE_LOG_EVENT_LOOP_SCHEDULED,
SERVICE_LOG_SSL,
)

DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
Expand Down Expand Up @@ -255,6 +259,64 @@ async def _async_dump_scheduled(call: ServiceCall) -> None:
arepr.maxstring = original_maxstring
arepr.maxother = original_maxother

async def _async_dump_ssl(call: ServiceCall) -> None:
"""Log all ssl objects in memory."""
# Imports deferred to avoid loading modules
# in memory since usually only one part of this
# integration is used at a time
import objgraph # pylint: disable=import-outside-toplevel

for obj in objgraph.by_type("SSLObject"):
obj = cast(ssl.SSLObject, obj)
try:
cert = obj.getpeercert()
except ValueError as ex:
cert = str(ex)
_LOGGER.critical(
"SSLObject %s server_hostname=%s peercert=%s",
obj,
obj.server_hostname,
cert,
)

for obj in objgraph.by_type("SSLProtocol"):
obj = cast(SSLProtocol, obj)
sock = None
if transport := obj._transport: # pylint: disable=protected-access
sock = transport.get_extra_info("socket")
_LOGGER.critical(
"SSLProtocol %s socket=%s",
obj,
sock,
)

for obj in objgraph.by_type("_SSLProtocolTransport"):
obj = cast(_SSLProtocolTransport, obj)
try:
ssl_proto = obj.get_protocol()
except AttributeError:
ssl_proto = None
try:
sock = obj.get_extra_info("socket")
except AttributeError:
sock = None
try:
ssl_object = obj.get_extra_info("ssl_object")
except AttributeError:
ssl_object = None
try:
peercert = obj.get_extra_info("peercert")
except (AttributeError, ValueError) as ex:
peercert = str(ex)
_LOGGER.critical(
"_SSLProtocolTransport %s ssl_proto=%s ssl_object=%s peercert=%s sock=%s",
obj,
ssl_proto,
ssl_object,
peercert,
sock,
)

async_register_admin_service(
hass,
DOMAIN,
Expand Down Expand Up @@ -349,6 +411,13 @@ async def _async_dump_scheduled(call: ServiceCall) -> None:
_async_dump_scheduled,
)

async_register_admin_service(
hass,
DOMAIN,
SERVICE_LOG_SSL,
_async_dump_ssl,
)

return True


Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/profiler/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,6 @@ log_thread_frames:
log_event_loop_scheduled:
name: Log event loop scheduled
description: Log what is scheduled in the event loop.
log_ssl:
name: Log SSL objects in memory
description: Log SSL objects to look for leaks.
54 changes: 54 additions & 0 deletions tests/components/profiler/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
CONF_SECONDS,
SERVICE_DUMP_LOG_OBJECTS,
SERVICE_LOG_EVENT_LOOP_SCHEDULED,
SERVICE_LOG_SSL,
SERVICE_LOG_THREAD_FRAMES,
SERVICE_LRU_STATS,
SERVICE_MEMORY,
Expand Down Expand Up @@ -387,3 +388,56 @@ def __repr__(self):
await hass.services.async_call(
DOMAIN, SERVICE_STOP_LOG_OBJECT_SOURCES, {}, blocking=True
)


async def test_log_ssl(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None:
"""Test logging ssl objects."""

entry = MockConfigEntry(domain=DOMAIN)
entry.add_to_hass(hass)

assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

class SSLProtocol:
def __init__(self):
"""Mock an SSLProtocol."""
self._transport = None

class SSLObject:
def __init__(self):
"""Mock an SSLObject."""
self._transport = None

def getpeercert(self, binary_form=False):
"""Mock getpeercert."""
return {"subject": (("commonName", "test"),)}

def server_hostname(self):
"""Mock server_hostname."""
return "test"

class _SSLProtocolTransport:
def __init__(self):
"""Mock an _SSLProtocolTransport."""

ssl_protocol = SSLProtocol()
ssl_object = SSLObject()
ssl_protocol_transport = _SSLProtocolTransport()
assert hass.services.has_service(DOMAIN, SERVICE_LOG_SSL)

def _mock_by_type(type_):
if type_ == "SSLProtocol":
return [ssl_protocol]
if type_ == "SSLObject":
return [ssl_object]
if type_ == "_SSLProtocolTransport":
return [ssl_protocol_transport]
raise ValueError("Unknown type")

with patch("objgraph.by_type", side_effect=_mock_by_type):
await hass.services.async_call(DOMAIN, SERVICE_LOG_SSL, blocking=True)

assert "SSLProtocol" in caplog.text
assert "SSLObject" in caplog.text
assert "_SSLProtocolTransport" in caplog.text