From 94958271e0537e1fbdd6ed8ce1358fe8947cba74 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Tue, 24 Dec 2024 14:58:19 +0100 Subject: [PATCH] remove incomplete httpx work --- logfire/_internal/cli.py | 54 ++++++++++++++++++++++++++++++++++++++-- tests/test_cli.py | 16 +----------- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/logfire/_internal/cli.py b/logfire/_internal/cli.py index 4a92fd7cc..c3650ea87 100644 --- a/logfire/_internal/cli.py +++ b/logfire/_internal/cli.py @@ -5,13 +5,16 @@ import argparse import functools import importlib +import importlib.metadata import importlib.util import logging import os import platform +import re import sys import warnings import webbrowser +from collections import defaultdict from pathlib import Path from typing import Any, Iterator, cast from urllib.parse import urljoin, urlparse @@ -195,6 +198,7 @@ def reader() -> Iterator[bytes]: 'urllib3', } OTEL_PACKAGE_LINK = {'aiohttp': 'aiohttp-client', 'tortoise_orm': 'tortoiseorm', 'scikit-learn': 'sklearn'} +STANDARD_LIBRARY_PACKAGES = {'urllib', 'sqlite3'} def parse_inspect(args: argparse.Namespace) -> None: @@ -207,11 +211,23 @@ def parse_inspect(args: argparse.Namespace) -> None: # Ignore warnings from packages that we don't control. warnings.simplefilter('ignore', category=UserWarning) + packages_to_inspect = OTEL_PACKAGES + if args.ignore_standard_library: + packages_to_inspect -= STANDARD_LIBRARY_PACKAGES + if args.ignore_package: + packages_to_inspect -= set(args.ignore_package) + + required_by = _build_required_by() + packages: dict[str, str] = {} - for name in OTEL_PACKAGES: + for name in packages_to_inspect: # Check if the package can be imported (without actually importing it). if importlib.util.find_spec(name) is None: continue + else: + # Skip packages that are not required by any other package. + if len([req for req in required_by[name] if not req.startswith('opentelemetry')]) == 0: + continue otel_package = OTEL_PACKAGE_LINK.get(name, name) otel_package_import = f'opentelemetry.instrumentation.{otel_package}' @@ -220,8 +236,10 @@ def parse_inspect(args: argparse.Namespace) -> None: packages[name] = otel_package # Drop packages that are dependencies of other packages. - if packages.get('starlette') and packages.get('fastapi'): + if packages.get('starlette') and len(required_by['starlette'] - {'fastapi'}) == 0: del packages['starlette'] + if packages.get('urllib3') and len(required_by['urllib3'] - {'requests'}) == 0: + del packages['urllib3'] for name, otel_package in sorted(packages.items()): package_name = otel_package.replace('.', '-') @@ -229,6 +247,9 @@ def parse_inspect(args: argparse.Namespace) -> None: link = f'[link={BASE_OTEL_INTEGRATION_URL}/{import_name}/{import_name}.html]opentelemetry-instrumentation-{package_name}[/link]' table.add_row(name, link) + if not packages: + return + console.print( 'The following packages from your environment have an OpenTelemetry instrumentation that is not installed:' ) @@ -244,6 +265,32 @@ def parse_inspect(args: argparse.Namespace) -> None: console.print('\n[bold blue]For further information, visit[/bold blue]', end=' ') console.print(f'[link={INTEGRATIONS_DOCS_URL}]{INTEGRATIONS_DOCS_URL}[/link]') + if args.explain: + console.print('\n[bold]Explanation:[/]') + for name, otel_package in packages.items(): + required_by_name = required_by[name] + if required_by_name: + console.print(f'\n[bold]{name}[/] is required by:') + for req in required_by_name: + console.print(f' - {req}') + + +# Package name should only include digits, letters, underscores, and hyphens. +ONLY_PACKAGE_NAME = re.compile(r'^(?P[a-zA-Z0-9_-]+)') + + +def _build_required_by() -> dict[str, set[str]]: + """Build a dictionary of packages and their dependencies.""" + required_by: defaultdict[str, set[str]] = defaultdict(set) + for package in importlib.metadata.distributions(): + if package.requires is None: + continue + for requirement in package.requires: + match = ONLY_PACKAGE_NAME.match(requirement) + assert match is not None + required_by[match.group('name')].add(package.metadata['Name']) + return required_by + def parse_auth(args: argparse.Namespace) -> None: """Authenticate with Logfire. @@ -438,6 +485,9 @@ def _main(args: list[str] | None = None) -> None: cmd_inspect = subparsers.add_parser('inspect', help=parse_inspect.__doc__) cmd_inspect.set_defaults(func=parse_inspect) + cmd_inspect.add_argument('--ignore-standard-library', action='store_true', help='ignore standard library packages') + cmd_inspect.add_argument('--ignore-package', action='append', help='ignore a package') + cmd_inspect.add_argument('--explain', action='store_true', help='explain the output') cmd_whoami = subparsers.add_parser('whoami', help=parse_whoami.__doc__) cmd_whoami.set_defaults(func=parse_whoami) diff --git a/tests/test_cli.py b/tests/test_cli.py index d5064d2a6..216bcc100 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -17,7 +17,7 @@ import logfire._internal.cli from logfire import VERSION -from logfire._internal.cli import OTEL_PACKAGES, main +from logfire._internal.cli import main from logfire._internal.config import LogfireCredentials, sanitize_project_name from logfire.exceptions import LogfireConfigError @@ -206,20 +206,6 @@ def test_inspect( assert capsys.readouterr().err.startswith('The following packages') -def test_inspect_drop_dependant_packages( - tmp_dir_cwd: Path, logfire_credentials: LogfireCredentials, capsys: pytest.CaptureFixture[str] -) -> None: - logfire_credentials.write_creds_file(tmp_dir_cwd / '.logfire') - with ExitStack() as stack: - find_spec = stack.enter_context(patch('importlib.util.find_spec')) - find_spec.side_effect = [True, None] * len(OTEL_PACKAGES) - - main(['inspect']) - output = capsys.readouterr().err - assert 'opentelemetry-instrumentation-fastapi' in output - assert 'opentelemetry-instrumentation-starlette' not in output - - @pytest.mark.parametrize('webbrowser_error', [False, True]) def test_auth(tmp_path: Path, webbrowser_error: bool) -> None: auth_file = tmp_path / 'default.toml'