diff --git a/irrexplorer_cli/helpers.py b/irrexplorer_cli/helpers.py index 4521caf..15b1df4 100644 --- a/irrexplorer_cli/helpers.py +++ b/irrexplorer_cli/helpers.py @@ -1,18 +1,24 @@ """Helper functions for the CLI.""" import ipaddress +import logging import re from typing import Any, Dict, List from irrexplorer_cli.models import PrefixInfo, PrefixResult +logger = logging.getLogger(__name__) + def validate_prefix_format(prefix_input: str) -> bool: """Validate IPv4 or IPv6 prefix format.""" + logger.debug("Validating prefix format: %s", prefix_input) try: ipaddress.ip_network(prefix_input) + logger.debug("Prefix validation successful") return True except ValueError: + logger.debug("Invalid prefix format") return False @@ -116,12 +122,14 @@ def format_as_sets(as_number: str, sets_data: Dict[str, Dict[str, List[str]]]) - async def find_least_specific_prefix(direct_overlaps: List[PrefixInfo]) -> str | None: """Find the least specific prefix from the overlaps.""" + logger.debug("Finding least specific prefix from %d overlaps", len(direct_overlaps)) least_specific = None for info in direct_overlaps: if "/" in info.prefix: _, mask = info.prefix.split("/") if least_specific is None or int(mask) < int(least_specific.split("/")[1]): least_specific = info.prefix + logger.debug("New least specific prefix found: %s", least_specific) return least_specific diff --git a/irrexplorer_cli/irrexplorer.py b/irrexplorer_cli/irrexplorer.py index e217751..fd84be1 100644 --- a/irrexplorer_cli/irrexplorer.py +++ b/irrexplorer_cli/irrexplorer.py @@ -1,5 +1,6 @@ """Core functionality for IRR Explorer CLI.""" +import logging from typing import Any, Dict, List, Optional, cast import backoff @@ -14,6 +15,8 @@ from .models import PrefixInfo +logger = logging.getLogger(__name__) + class IrrExplorer: """IRR Explorer API client for prefix information retrieval.""" @@ -28,18 +31,17 @@ def __init__(self, base_url: str = "https://irrexplorer.nlnog.net") -> None: @backoff.on_exception(backoff.expo, (httpx.HTTPError, httpx.RequestError), max_tries=3, max_time=300) async def fetch_prefix_info(self, prefix: str) -> List[PrefixInfo]: """Fetch prefix information from IRR Explorer API.""" + logger.debug("Fetching prefix info for: %s", prefix) try: url = f"{self.base_url}/api/prefixes/prefix/{prefix}" + logger.debug("Making API request to: %s", url) response = await self.client.get(url) response.raise_for_status() data = response.json() - if not data: - return [] + logger.debug("Received response data: %s", data) return [PrefixInfo(**item) for item in data] except httpx.TimeoutException: - self.console.print( - f"[yellow]Request timed out while fetching info for {prefix}. The server might be busy.[/yellow]" - ) + logger.error("Request timeout for prefix: %s", prefix) return [] @backoff.on_exception(backoff.expo, (httpx.HTTPError, httpx.RequestError), max_tries=3, max_time=300) @@ -171,7 +173,9 @@ async def get_status_style(self, category: str) -> str: async def display_prefix_info(self, direct_overlaps: List[PrefixInfo]) -> None: """Display prefix information in Rich panels.""" + logger.debug("Displaying prefix info for %d overlaps", len(direct_overlaps)) if not direct_overlaps: + logger.debug("No prefix information found") self.console.print("[yellow]No prefix information found[/yellow]") return diff --git a/irrexplorer_cli/main.py b/irrexplorer_cli/main.py index ce63f20..a149944 100644 --- a/irrexplorer_cli/main.py +++ b/irrexplorer_cli/main.py @@ -1,6 +1,7 @@ """Command-line interface for IRR Explorer queries.""" import asyncio +import logging from importlib.metadata import version from typing import Optional @@ -19,6 +20,17 @@ context_settings={"help_option_names": ["-h", "--help"]}, ) console = Console() +logger = logging.getLogger(__name__) + + +def setup_logging(debug: bool) -> None: + """Configure logging based on debug flag.""" + log_level = logging.DEBUG if debug else logging.INFO + logging.basicConfig(level=log_level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") + + # set httpx logger level based on debug flag + httpx_logger = logging.getLogger("httpx") + httpx_logger.setLevel(logging.DEBUG if debug else logging.WARNING) def version_display(display_version: bool) -> None: @@ -33,10 +45,13 @@ def callback( ctx: typer.Context, _: bool = typer.Option(None, "--version", "-v", callback=version_display, is_eager=True), base_url: Optional[str] = typer.Option(None, "--url", "-u", help="Base URL for IRR Explorer API"), + debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug logging"), ) -> None: """Query IRR Explorer for prefix information.""" ctx.ensure_object(dict) ctx.obj["base_url"] = base_url + setup_logging(debug) + logger.debug("CLI initialized with base_url: %s", base_url) @app.command(no_args_is_help=True) diff --git a/irrexplorer_cli/models.py b/irrexplorer_cli/models.py index f4b9091..219e2fc 100644 --- a/irrexplorer_cli/models.py +++ b/irrexplorer_cli/models.py @@ -1,8 +1,11 @@ """Data models for IRR Explorer API responses.""" -from typing import Dict, List, Optional +import logging +from typing import Any, Dict, List, Optional -from pydantic import BaseModel +from pydantic import BaseModel, model_validator + +logger = logging.getLogger(__name__) class BaseRoute(BaseModel): @@ -43,6 +46,20 @@ class PrefixInfo(BaseModel): prefixSortKey: str goodnessOverall: int + def __init__(self, **data: Any) -> None: + logger.debug("Initializing PrefixInfo with data: %s", data) + super().__init__(**data) + logger.debug("PrefixInfo initialized successfully for prefix: %s", self.prefix) + + @model_validator(mode="after") + def validate_prefix_info(self) -> "PrefixInfo": + """Validate the prefix information after model creation.""" + logger.debug("Validating PrefixInfo for prefix: %s", self.prefix) + logger.debug( + "Category: %s, RIR: %s, BGP Origins count: %d", self.categoryOverall, self.rir, len(self.bgpOrigins) + ) + return self + class AsResponse(BaseModel): """Response model for AS queries.""" diff --git a/irrexplorer_cli/queries.py b/irrexplorer_cli/queries.py index 2c5505f..5cdae5f 100644 --- a/irrexplorer_cli/queries.py +++ b/irrexplorer_cli/queries.py @@ -1,6 +1,7 @@ """Query functions for the CLI tool.""" import json +import logging from typing import Optional import httpx @@ -15,6 +16,8 @@ ) from irrexplorer_cli.irrexplorer import IrrDisplay, IrrExplorer +logger = logging.getLogger(__name__) + async def process_overlaps(explorer: IrrExplorer, least_specific: str) -> None: """Process and print overlapping prefixes.""" @@ -28,12 +31,17 @@ async def process_overlaps(explorer: IrrExplorer, least_specific: str) -> None: async def async_prefix_query(pfx: str, output_format: Optional[str] = None, base_url: Optional[str] = None) -> None: """Execute asynchronous prefix query and display results.""" + logger.debug("Starting prefix query for: %s", pfx) + logger.debug("Output format: %s, Base URL: %s", output_format, base_url) explorer = IrrExplorer(base_url=base_url) if base_url else IrrExplorer() display = IrrDisplay() try: direct_overlaps = await explorer.fetch_prefix_info(pfx) + logger.debug("Received %d direct overlaps", len(direct_overlaps)) + if output_format == "json": + logger.debug("Formatting output as JSON") json_data = [result.model_dump() for result in direct_overlaps] print(json.dumps(json_data, indent=2)) elif output_format == "csv":