Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ logging:
level: DEBUG # Set to DEBUG for detailed logs, or INFO for less verbosity
format: "%(asctime)s [%(levelname)s] - %(module)s - %(message)s"
enable_colors: true
debug_file_logging: true # Set to true to enable debug file logging

# Feature-specific log levels - set all to INFO to match global level
feature_levels:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ logging:
level: DEBUG # Set to DEBUG for detailed logs, or INFO for less verbosity
format: "%(asctime)s [%(levelname)s] - %(module)s - %(message)s"
enable_colors: true
debug_file_logging: true # Set to true to enable debug file logging

# Feature-specific log levels - set all to INFO to match global level
feature_levels:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ logging:
level: DEBUG
format: "%(asctime)s [%(levelname)s] - %(module)s - %(message)s"
enable_colors: true
debug_file_logging: true

feature_levels:
command_generation: DEBUG
Expand Down
139 changes: 139 additions & 0 deletions panther/cli/commands/build_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Build Metrics Command.

View and export build/test metrics collected by the PANTHER builder.
"""

import json
import sys
from pathlib import Path

import click

from panther.builder_metrics.cli import format_duration, format_size, format_timestamp
from panther.builder_metrics.storage import JSONLinesStorage
from panther.cli.core.base import (
featured_example,
handle_errors,
pass_context_and_setup_logging,
)


def _get_storage() -> JSONLinesStorage:
project_root = Path.cwd()
return JSONLinesStorage(project_root / ".panther-metrics")


@featured_example("panther build-metrics list")
@click.group("build-metrics")
def build_metrics():
r"""View and export build/test metrics.

Reads metrics collected during package builds and test runs
from .panther-metrics/ JSONL storage.

\b
Examples:
panther build-metrics list # List recent records
panther build-metrics show <run-id> # Show record details
panther build-metrics export # Export to JSON
"""


@build_metrics.command("list")
@click.option(
"--limit", "-n", type=int, default=20, help="Max records to show (default: 20)"
)
@handle_errors
@pass_context_and_setup_logging
def list_records(_ctx, limit):

Check warning on line 48 in panther/cli/commands/build_metrics.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

panther/cli/commands/build_metrics.py#L48

Method list_records has a cyclomatic complexity of 10 (limit is 8)
"""List recent build/test metrics records."""
storage = _get_storage()
records = storage.read_records(limit=limit)

if not records:
click.echo("No metrics records found.")
return

click.echo(f"{'Run ID':<36} {'Type':<8} {'Timestamp':<19} {'Key Metrics'}")
click.echo("-" * 80)

for record in records:
run_id = record.get("run_id", "unknown")[:35]
record_type = record.get("type", "unknown")
timestamp = format_timestamp(record.get("timestamp", ""))
metrics = record.get("metrics", {})
key_metrics = []

if record_type == "builder":
if "container.build_seconds" in metrics:
key_metrics.append(
f"build: {format_duration(metrics['container.build_seconds'])}"
)
if "container.image_mb" in metrics:
key_metrics.append(
f"image: {format_size(metrics['container.image_mb'])}"
)
elif record_type == "tests":
if "pytest.total_seconds" in metrics:
key_metrics.append(
f"duration: {format_duration(metrics['pytest.total_seconds'])}"
)
if "pytest.passed" in metrics and "pytest.failed" in metrics:
key_metrics.append(
f"passed: {int(metrics['pytest.passed'])}, failed: {int(metrics['pytest.failed'])}"
)

click.echo(
f"{run_id:<36} {record_type:<8} {timestamp:<19} {', '.join(key_metrics) or 'no metrics'}"
)


@build_metrics.command("show")
@click.argument("run_id")
@handle_errors
@pass_context_and_setup_logging
def show_record(_ctx, run_id):
"""Show detailed information about a specific metrics record."""
storage = _get_storage()
record = storage.get_record_by_id(run_id)

if not record:
click.echo(f"No record found with run ID: {run_id}")
return

click.echo(f"Run ID: {record.get('run_id', 'unknown')}")
click.echo(f"Type: {record.get('type', 'unknown')}")
click.echo(f"Timestamp: {format_timestamp(record.get('timestamp', ''))}")
click.echo(f"Git Commit: {record.get('git_commit', 'unknown')}")
click.echo()

metrics = record.get("metrics", {})
if metrics:
click.echo("Metrics:")
for name, value in sorted(metrics.items()):
if name.endswith("_seconds"):
click.echo(f" {name}: {format_duration(value)}")
elif name.endswith("_mb"):
click.echo(f" {name}: {format_size(value)}")
elif isinstance(value, float):
click.echo(f" {name}: {value:.2f}")
else:
click.echo(f" {name}: {value}")


@build_metrics.command("export")
@click.option("--output", "-o", type=click.Path(), help="Output file (default: stdout)")
@click.option("--limit", "-n", type=int, help="Max records to export")
@handle_errors
@pass_context_and_setup_logging
def export_records(_ctx, output, limit):
"""Export metrics records to JSON."""
storage = _get_storage()
records = storage.read_records(limit=limit)

if output:
with open(output, "w") as f:
json.dump(records, f, indent=2)
click.echo(f"Exported {len(records)} records to {output}")
else:
json.dump(records, sys.stdout, indent=2)
122 changes: 122 additions & 0 deletions panther/cli/commands/debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Debug Command.

Developer introspection tools for the PANTHER event and observer systems.
"""

import click
from termcolor import colored

from panther.cli.core.base import (
featured_example,
handle_errors,
pass_context_and_setup_logging,
)


@featured_example("panther debug events list-types")
@click.group()
def debug():
r"""Developer debugging and introspection tools.

Inspect PANTHER internals: event types, emitters, observers.

\b
Examples:
panther debug events list-types # List event types
panther debug events list-emitters # List event emitters
panther debug observers list-types # List observer types
"""


@debug.group()
def events():
"""Inspect the event system."""


@events.command("list-types")
@handle_errors
@pass_context_and_setup_logging
def list_event_types(_ctx):
"""List all known event types with importance levels."""
from panther.core.events.event_summarizer import EventSummarizer

event_types = EventSummarizer.IMPORTANT_EVENT_TYPES
batchable = EventSummarizer.BATCHABLE_EVENTS

click.echo(colored("Event Types:", "blue", attrs=["bold"]))
click.echo(f" {'Event Type':<45} {'Importance':<12} {'Batchable'}")
click.echo(" " + "-" * 70)

for event_type, importance in sorted(event_types.items()):
batch_flag = "yes" if event_type in batchable else ""
click.echo(f" {event_type:<45} {importance.name:<12} {batch_flag}")

click.echo()
click.echo(f"Total: {len(event_types)} event types, {len(batchable)} batchable")


@events.command("list-emitters")
@handle_errors
@pass_context_and_setup_logging
def list_emitters(_ctx):
"""List registered event emitter types."""
from panther.core.events.assertion.emitter import AssertionEventEmitter
from panther.core.events.environment.emitter import EnvironmentEventEmitter
from panther.core.events.experiment.emitter import ExperimentEventEmitter
from panther.core.events.metrics.emitter import MetricsEventEmitter
from panther.core.events.plugin.emitter import PluginEventEmitter
from panther.core.events.service.emitter import ServiceEventEmitter
from panther.core.events.step.emitter import StepEventEmitter
from panther.core.events.test.emitter import TestEventEmitter

# These are the emitter types managed by EmitterRegistry.get_emitter()
emitter_map = {
"experiment": ExperimentEventEmitter,
"service": ServiceEventEmitter,
"environment": EnvironmentEventEmitter,
"step": StepEventEmitter,
"plugin": PluginEventEmitter,
"assertion": AssertionEventEmitter,
"metrics": MetricsEventEmitter,
"test": TestEventEmitter,
}

click.echo(colored("Event Emitters:", "blue", attrs=["bold"]))
click.echo(f" {'Category':<20} {'Class'}")
click.echo(" " + "-" * 55)

for category, cls in sorted(emitter_map.items()):
click.echo(f" {category:<20} {cls.__module__}.{cls.__name__}")

click.echo()
click.echo(f"Total: {len(emitter_map)} emitter types")
click.echo()
click.echo("Note: 'test' emitters are created on demand (one per test case).")


@debug.group()
def observers():
"""Inspect the observer system."""


@observers.command("list-types")
@handle_errors
@pass_context_and_setup_logging
def list_observer_types(_ctx):
"""List available observer types."""
from panther.core.observer.factory import ObserverFactory

factory = ObserverFactory()
available = factory.get_available_types()
registered_types = factory._registered_types

click.echo(colored("Observer Types:", "blue", attrs=["bold"]))
click.echo(f" {'Type Name':<20} {'Class'}")
click.echo(" " + "-" * 55)

for type_name in sorted(available):
cls = registered_types[type_name]
click.echo(f" {type_name:<20} {cls.__module__}.{cls.__name__}")

click.echo()
click.echo(f"Total: {len(available)} observer types")
Loading
Loading