Skip to content

Commit d82300f

Browse files
SNOW-3012459: Extend core telemetry (#2742)
1 parent fef4999 commit d82300f

File tree

2 files changed

+153
-13
lines changed

2 files changed

+153
-13
lines changed

src/snowflake/connector/_utils.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import platform
88
import string
99
import threading
10+
import time
1011
from enum import Enum
1112
from inspect import stack
1213
from pathlib import Path
@@ -141,6 +142,7 @@ def __init__(self):
141142
self._version: bytes | None = None
142143
self._error: Exception | None = None
143144
self._path: str | None = None
145+
self._load_time: float | None = None
144146

145147
@staticmethod
146148
def _detect_os() -> str:
@@ -229,7 +231,7 @@ def _get_lib_name() -> str:
229231
return "libsf_mini_core.so"
230232

231233
@staticmethod
232-
def _get_core_path() -> Path:
234+
def _get_core_path():
233235
"""Get the path to the minicore library for the current platform."""
234236
subdir = _CoreLoader._get_platform_subdir()
235237
lib_name = _CoreLoader._get_lib_name()
@@ -253,20 +255,49 @@ def _load_minicore(path: str) -> ctypes.CDLL:
253255
core = ctypes.CDLL(str(lib_path))
254256
return core
255257

258+
def get_present_binaries(self) -> str:
259+
present_binaries = []
260+
try:
261+
minicore_files = importlib.resources.files("snowflake.connector.minicore")
262+
# Iterate through all items in the minicore module
263+
for item in minicore_files.iterdir():
264+
# Skip non-platform directories like __pycache__
265+
if item.is_dir() and not item.name.startswith("__"):
266+
# This is a platform subdirectory
267+
platform_name = item.name
268+
try:
269+
# List all files in this subdirectory
270+
for binary_file in item.iterdir():
271+
if binary_file.is_file():
272+
# Store as "platform/filename"
273+
present_binaries.append(
274+
f"{platform_name}/{binary_file.name}"
275+
)
276+
except Exception as e:
277+
logger.debug(f"Error listing binaries in {platform_name}: {e}")
278+
except Exception as e:
279+
logger.debug(f"Error populating present binaries: {e}")
280+
281+
return ",".join(present_binaries)
282+
256283
def _is_core_disabled(self) -> bool:
257284
value = str(os.getenv("SNOWFLAKE_DISABLE_MINICORE", None)).lower()
258285
return value in ["1", "true"]
259286

260287
def _load(self) -> None:
288+
start_time = time.perf_counter()
261289
try:
262290
path = self._get_core_path()
291+
self._path = str(path)
263292
core = self._load_minicore(path)
264293
self._register_functions(core)
265294
self._version = core.sf_core_full_version()
266295
self._error = None
267-
self._path = str(path)
268296
except Exception as err:
269297
self._error = err
298+
end_time = time.perf_counter()
299+
# Store load time in milliseconds (with sub-millisecond precision)
300+
self._load_time = (end_time - start_time) * 1000
270301

271302
def load(self):
272303
"""Spawn a separate thread to load the minicore library (non-blocking)."""
@@ -291,6 +322,10 @@ def get_core_version(self) -> str | None:
291322
def get_file_name(self) -> str:
292323
return self._path
293324

325+
def get_load_time(self) -> float | None:
326+
"""Return the time it took to load the minicore binary in milliseconds."""
327+
return self._load_time
328+
294329

295330
_core_loader = _CoreLoader()
296331

@@ -308,5 +343,7 @@ def build_minicore_usage_for_telemetry() -> dict[str, str | None]:
308343
"OS": OPERATING_SYSTEM,
309344
"OS_VERSION": OS_VERSION,
310345
"CORE_LOAD_ERROR": _core_loader.get_load_error(),
346+
"CORE_BINARIES_PRESENT": _core_loader.get_present_binaries(),
347+
"CORE_LOAD_TIME": _core_loader.get_load_time(),
311348
**build_minicore_usage_for_session(),
312349
}

test/unit/test_util.py

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,17 @@ def test_e2e(self):
3838
sleep(2)
3939
assert loader.get_load_error() == str(None)
4040
assert loader.get_core_version() == "0.0.1"
41+
# Verify load time was measured
42+
assert loader.get_load_time() is not None
43+
assert loader.get_load_time() >= 0
4144

4245
def test_core_loader_initialization(self):
4346
"""Test that _CoreLoader initializes with None values."""
4447
loader = _CoreLoader()
4548
assert loader._version is None
4649
assert loader._error is None
4750
assert loader._path is None
51+
assert loader._load_time is None
4852

4953
@pytest.mark.parametrize(
5054
"system,expected",
@@ -194,7 +198,7 @@ def test_register_functions(self):
194198
assert mock_core.sf_core_full_version.restype == ctypes.c_char_p
195199

196200
def test_load_minicore(self):
197-
"""Test that _load_minicore loads the library."""
201+
"""Test that _load_minicore loads the library correctly."""
198202
mock_path = mock.MagicMock()
199203
mock_lib_path = "/path/to/libsf_mini_core.so"
200204

@@ -255,7 +259,7 @@ def test_load_skips_loading_when_core_disabled(self):
255259
def test_load_success(self):
256260
"""Test successful load of the core library."""
257261
loader = _CoreLoader()
258-
mock_path = mock.MagicMock()
262+
mock_path = "/path/to/libsf_mini_core.so"
259263
mock_core = mock.MagicMock()
260264
mock_version = b"1.2.3"
261265
mock_core.sf_core_full_version = mock.MagicMock(return_value=mock_version)
@@ -270,15 +274,18 @@ def test_load_success(self):
270274
with mock.patch.object(
271275
loader, "_register_functions"
272276
) as mock_register:
273-
loader.load()
274-
sleep(2)
275-
276-
mock_get_path.assert_called_once()
277-
mock_load.assert_called_once_with(mock_path)
278-
mock_register.assert_called_once_with(mock_core)
279-
assert loader._version == mock_version
280-
assert loader._error is None
281-
assert loader._path == str(mock_path)
277+
with mock.patch("time.perf_counter", side_effect=[0.0, 0.0155]):
278+
loader.load()
279+
sleep(2)
280+
281+
mock_get_path.assert_called_once()
282+
mock_load.assert_called_once_with(mock_path)
283+
mock_register.assert_called_once_with(mock_core)
284+
assert loader._version == mock_version
285+
assert loader._error is None
286+
assert loader._path == mock_path
287+
# (0.0155 - 0.0) * 1000 = 15.5 ms
288+
assert loader._load_time == 15.5
282289

283290
def test_load_failure(self):
284291
"""Test that load captures exceptions."""
@@ -349,6 +356,102 @@ def test_get_file_name_no_path(self):
349356

350357
assert result is None
351358

359+
def test_get_load_time_with_time(self):
360+
"""Test get_load_time returns the load time when it has been set."""
361+
loader = _CoreLoader()
362+
loader._load_time = 42.5
363+
364+
result = loader.get_load_time()
365+
366+
assert result == 42.5
367+
368+
def test_get_load_time_no_time(self):
369+
"""Test get_load_time returns None when no load time exists."""
370+
loader = _CoreLoader()
371+
372+
result = loader.get_load_time()
373+
374+
assert result is None
375+
376+
def test_get_present_binaries_contains_expected_paths(self):
377+
"""Test get_present_binaries returns binaries for expected paths."""
378+
loader = _CoreLoader()
379+
380+
result = loader.get_present_binaries()
381+
382+
assert isinstance(result, str)
383+
assert result != ""
384+
385+
def test_get_present_binaries_with_mocked_structure(self, tmp_path):
386+
"""Test get_present_binaries with mocked directory structure."""
387+
loader = _CoreLoader()
388+
389+
# Create a temporary directory structure mimicking minicore layout
390+
# Create platform directories with binary files
391+
linux_dir = tmp_path / "linux_x86_64_glibc"
392+
linux_dir.mkdir()
393+
(linux_dir / "libsf_mini_core.so").write_text("fake binary content")
394+
395+
macos_dir = tmp_path / "macos_aarch64"
396+
macos_dir.mkdir()
397+
(macos_dir / "libsf_mini_core.dylib").write_text("fake binary content")
398+
399+
windows_dir = tmp_path / "windows_x86_64"
400+
windows_dir.mkdir()
401+
(windows_dir / "sf_mini_core.dll").write_text("fake binary content")
402+
403+
# Create a __pycache__ directory that should be ignored
404+
pycache_dir = tmp_path / "__pycache__"
405+
pycache_dir.mkdir()
406+
(pycache_dir / "some_file.pyc").write_text("cached file")
407+
408+
# Mock importlib.resources.files to return our temp directory
409+
with mock.patch("importlib.resources.files") as mock_files:
410+
mock_files.return_value = tmp_path
411+
412+
result = loader.get_present_binaries()
413+
414+
# Verify the function was called with correct module name
415+
mock_files.assert_called_once_with("snowflake.connector.minicore")
416+
417+
# Parse the result
418+
binaries = result.split(",")
419+
assert len(binaries) == 3
420+
421+
# Verify all expected binaries are present
422+
assert "linux_x86_64_glibc/libsf_mini_core.so" in binaries
423+
assert "macos_aarch64/libsf_mini_core.dylib" in binaries
424+
assert "windows_x86_64/sf_mini_core.dll" in binaries
425+
426+
# Verify __pycache__ files are not included
427+
assert not any("__pycache__" in binary for binary in binaries)
428+
429+
def test_get_present_binaries_with_empty_directory(self, tmp_path):
430+
"""Test get_present_binaries returns empty string for empty directory."""
431+
loader = _CoreLoader()
432+
433+
# Create an empty temp directory
434+
# Mock importlib.resources.files to return our temp directory
435+
with mock.patch("importlib.resources.files") as mock_files:
436+
mock_files.return_value = tmp_path
437+
438+
result = loader.get_present_binaries()
439+
440+
assert result == ""
441+
442+
def test_get_present_binaries_handles_exceptions(self):
443+
"""Test get_present_binaries handles exceptions gracefully."""
444+
loader = _CoreLoader()
445+
446+
# Mock importlib.resources.files to raise an exception
447+
with mock.patch("importlib.resources.files") as mock_files:
448+
mock_files.side_effect = Exception("Failed to access resources")
449+
450+
# Should not raise, but return empty string
451+
result = loader.get_present_binaries()
452+
453+
assert result == ""
454+
352455

353456
def test_importing_snowflake_connector_triggers_core_loader_load():
354457
"""Test that importing snowflake.connector triggers core_loader.load()."""

0 commit comments

Comments
 (0)