Skip to content

Commit 06e1ead

Browse files
committed
Enhance logging functionality
1 parent 47ca5a3 commit 06e1ead

File tree

9 files changed

+532
-20
lines changed

9 files changed

+532
-20
lines changed

docs/reference/api-full.md

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ For a curated, example-driven API guide, see **[api.md](api.md)**.
1010
> - **[CLI Reference](cli.md)** - Command-line interface
1111
> - **[DSL Reference](dsl.md)** - YAML syntax guide
1212
13-
**Generated from source code on:** June 13, 2025 at 17:13 UTC
13+
**Generated from source code on:** June 13, 2025 at 17:50 UTC
1414

15-
**Modules auto-discovered:** 38
15+
**Modules auto-discovered:** 39
1616

1717
---
1818

@@ -439,6 +439,55 @@ Attributes:
439439

440440
---
441441

442+
## ngraph.logging
443+
444+
Centralized logging configuration for NetGraph.
445+
446+
### disable_debug_logging() -> None
447+
448+
Disable debug logging, set to INFO level.
449+
450+
### enable_debug_logging() -> None
451+
452+
Enable debug logging for the entire package.
453+
454+
### get_logger(name: str) -> logging.Logger
455+
456+
Get a logger with NetGraph's standard configuration.
457+
458+
This is the main function that should be used throughout the package.
459+
All loggers will inherit from the root 'ngraph' logger configuration.
460+
461+
Args:
462+
name: Logger name (typically __name__ from calling module).
463+
464+
Returns:
465+
Configured logger instance.
466+
467+
### reset_logging() -> None
468+
469+
Reset logging configuration (mainly for testing).
470+
471+
### set_global_log_level(level: int) -> None
472+
473+
Set the log level for all NetGraph loggers.
474+
475+
Args:
476+
level: Logging level (e.g., logging.DEBUG, logging.INFO).
477+
478+
### setup_root_logger(level: int = 20, format_string: Optional[str] = None, handler: Optional[logging.Handler] = None) -> None
479+
480+
Set up the root NetGraph logger with a single handler.
481+
482+
This should only be called once to avoid duplicate handlers.
483+
484+
Args:
485+
level: Logging level (default: INFO).
486+
format_string: Custom format string (optional).
487+
handler: Custom handler (optional, defaults to StreamHandler).
488+
489+
---
490+
442491
## ngraph.network
443492

444493
### Link
@@ -1753,13 +1802,17 @@ Attributes:
17531802

17541803
Base class for all workflow steps.
17551804

1805+
All workflow steps are automatically logged with execution timing information.
1806+
17561807
**Attributes:**
17571808

17581809
- `name` (str)
17591810

17601811
**Methods:**
17611812

1762-
- `run(self, scenario: 'Scenario') -> 'None'`
1813+
- `execute(self, scenario: "'Scenario'") -> 'None'`
1814+
- Execute the workflow step with automatic logging.
1815+
- `run(self, scenario: "'Scenario'") -> 'None'`
17631816
- Execute the workflow step logic.
17641817

17651818
### register_workflow_step(step_type: 'str')
@@ -1780,6 +1833,8 @@ A workflow step that builds a StrictMultiDiGraph from scenario.network.
17801833

17811834
**Methods:**
17821835

1836+
- `execute(self, scenario: "'Scenario'") -> 'None'`
1837+
- Execute the workflow step with automatic logging.
17831838
- `run(self, scenario: 'Scenario') -> 'None'`
17841839
- Execute the workflow step logic.
17851840

@@ -1820,6 +1875,8 @@ Attributes:
18201875

18211876
**Methods:**
18221877

1878+
- `execute(self, scenario: "'Scenario'") -> 'None'`
1879+
- Execute the workflow step with automatic logging.
18231880
- `run(self, scenario: "'Scenario'") -> 'None'`
18241881
- Execute the capacity envelope analysis workflow step.
18251882

@@ -1853,6 +1910,8 @@ Attributes:
18531910

18541911
**Methods:**
18551912

1913+
- `execute(self, scenario: "'Scenario'") -> 'None'`
1914+
- Execute the workflow step with automatic logging.
18561915
- `run(self, scenario: 'Scenario') -> 'None'`
18571916
- Executes the capacity probe by computing max flow between node groups
18581917

ngraph/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from __future__ import annotations
22

3-
from . import cli, config, transform
3+
from . import cli, config, logging, transform
44
from .results_artifacts import CapacityEnvelope, PlacementResultSet, TrafficMatrixSet
55

66
__all__ = [
77
"cli",
88
"config",
9+
"logging",
910
"transform",
1011
"CapacityEnvelope",
1112
"PlacementResultSet",

ngraph/cli.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,46 @@
22

33
import argparse
44
import json
5+
import logging
6+
import sys
57
from pathlib import Path
68
from typing import Any, Dict, List, Optional
79

10+
from ngraph.logging import get_logger, set_global_log_level
811
from ngraph.scenario import Scenario
912

13+
logger = get_logger(__name__)
14+
1015

1116
def _run_scenario(path: Path, output: Optional[Path]) -> None:
1217
"""Run a scenario file and store results as JSON."""
18+
logger.info(f"Loading scenario from: {path}")
1319

14-
yaml_text = path.read_text()
15-
scenario = Scenario.from_yaml(yaml_text)
16-
scenario.run()
20+
try:
21+
yaml_text = path.read_text()
22+
scenario = Scenario.from_yaml(yaml_text)
1723

18-
results_dict: Dict[str, Dict[str, Any]] = scenario.results.to_dict()
19-
json_str = json.dumps(results_dict, indent=2, default=str)
20-
if output:
21-
output.write_text(json_str)
22-
else:
23-
print(json_str)
24+
logger.info("Starting scenario execution")
25+
scenario.run()
26+
logger.info("Scenario execution completed successfully")
27+
28+
logger.info("Serializing results to JSON")
29+
results_dict: Dict[str, Dict[str, Any]] = scenario.results.to_dict()
30+
json_str = json.dumps(results_dict, indent=2, default=str)
31+
32+
if output:
33+
logger.info(f"Writing results to: {output}")
34+
output.write_text(json_str)
35+
logger.info("Results written successfully")
36+
else:
37+
print(json_str)
38+
39+
except FileNotFoundError:
40+
logger.error(f"Scenario file not found: {path}")
41+
sys.exit(1)
42+
except Exception as e:
43+
logger.error(f"Failed to run scenario: {type(e).__name__}: {e}")
44+
sys.exit(1)
2445

2546

2647
def main(argv: Optional[List[str]] = None) -> None:
@@ -31,6 +52,15 @@ def main(argv: Optional[List[str]] = None) -> None:
3152
is used.
3253
"""
3354
parser = argparse.ArgumentParser(prog="ngraph")
55+
56+
# Global options
57+
parser.add_argument(
58+
"--verbose", "-v", action="store_true", help="Enable verbose (DEBUG) logging"
59+
)
60+
parser.add_argument(
61+
"--quiet", "-q", action="store_true", help="Enable quiet mode (WARNING+ only)"
62+
)
63+
3464
subparsers = parser.add_subparsers(dest="command", required=True)
3565

3666
run_parser = subparsers.add_parser("run", help="Run a scenario")
@@ -45,6 +75,15 @@ def main(argv: Optional[List[str]] = None) -> None:
4575

4676
args = parser.parse_args(argv)
4777

78+
# Configure logging based on arguments
79+
if args.verbose:
80+
set_global_log_level(logging.DEBUG)
81+
logger.debug("Debug logging enabled")
82+
elif args.quiet:
83+
set_global_log_level(logging.WARNING)
84+
else:
85+
set_global_log_level(logging.INFO)
86+
4887
if args.command == "run":
4988
_run_scenario(args.scenario, args.results)
5089

ngraph/explorer.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
from __future__ import annotations
22

3-
import logging
43
from dataclasses import dataclass, field
54
from typing import Dict, List, Optional, Set
65

76
from ngraph.components import ComponentsLibrary
7+
from ngraph.logging import get_logger
88
from ngraph.network import Network, Node
99

10-
logger = logging.getLogger(__name__)
11-
logger.setLevel(logging.DEBUG)
10+
logger = get_logger(__name__)
1211

1312

1413
@dataclass

ngraph/logging.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Centralized logging configuration for NetGraph."""
2+
3+
import logging
4+
import sys
5+
from typing import Optional
6+
7+
# Flag to track if we've already set up the root logger
8+
_ROOT_LOGGER_CONFIGURED = False
9+
10+
11+
def setup_root_logger(
12+
level: int = logging.INFO,
13+
format_string: Optional[str] = None,
14+
handler: Optional[logging.Handler] = None,
15+
) -> None:
16+
"""Set up the root NetGraph logger with a single handler.
17+
18+
This should only be called once to avoid duplicate handlers.
19+
20+
Args:
21+
level: Logging level (default: INFO).
22+
format_string: Custom format string (optional).
23+
handler: Custom handler (optional, defaults to StreamHandler).
24+
"""
25+
global _ROOT_LOGGER_CONFIGURED
26+
27+
if _ROOT_LOGGER_CONFIGURED:
28+
return
29+
30+
root_logger = logging.getLogger("ngraph")
31+
root_logger.setLevel(level)
32+
33+
# Clear any existing handlers to avoid duplicates
34+
root_logger.handlers.clear()
35+
36+
# Default format with timestamps, level, logger name, and message
37+
if format_string is None:
38+
format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
39+
40+
# Default to console output, but allow override for testing
41+
if handler is None:
42+
handler = logging.StreamHandler(sys.stdout)
43+
44+
formatter = logging.Formatter(format_string)
45+
handler.setFormatter(formatter)
46+
root_logger.addHandler(handler)
47+
48+
# Let logs propagate to root logger so pytest can capture them
49+
root_logger.propagate = True
50+
51+
_ROOT_LOGGER_CONFIGURED = True
52+
53+
_ROOT_LOGGER_CONFIGURED = True
54+
55+
56+
def get_logger(name: str) -> logging.Logger:
57+
"""Get a logger with NetGraph's standard configuration.
58+
59+
This is the main function that should be used throughout the package.
60+
All loggers will inherit from the root 'ngraph' logger configuration.
61+
62+
Args:
63+
name: Logger name (typically __name__ from calling module).
64+
65+
Returns:
66+
Configured logger instance.
67+
"""
68+
# Ensure root logger is set up
69+
setup_root_logger()
70+
71+
# Get the logger - it will inherit from the root ngraph logger
72+
logger = logging.getLogger(name)
73+
74+
# Don't add handlers to child loggers - they inherit from root
75+
# Just set the level
76+
logger.setLevel(logging.NOTSET) # Inherit from parent
77+
78+
return logger
79+
80+
81+
def set_global_log_level(level: int) -> None:
82+
"""Set the log level for all NetGraph loggers.
83+
84+
Args:
85+
level: Logging level (e.g., logging.DEBUG, logging.INFO).
86+
"""
87+
# Ensure root logger is set up
88+
setup_root_logger()
89+
90+
# Set the root level for all ngraph loggers
91+
root_logger = logging.getLogger("ngraph")
92+
root_logger.setLevel(level)
93+
94+
# Also update handlers to respect the new level
95+
for handler in root_logger.handlers:
96+
handler.setLevel(level)
97+
98+
99+
def enable_debug_logging() -> None:
100+
"""Enable debug logging for the entire package."""
101+
set_global_log_level(logging.DEBUG)
102+
103+
104+
def disable_debug_logging() -> None:
105+
"""Disable debug logging, set to INFO level."""
106+
set_global_log_level(logging.INFO)
107+
108+
109+
def reset_logging() -> None:
110+
"""Reset logging configuration (mainly for testing)."""
111+
global _ROOT_LOGGER_CONFIGURED
112+
_ROOT_LOGGER_CONFIGURED = False
113+
114+
# Clear any existing handlers from ngraph logger
115+
root_logger = logging.getLogger("ngraph")
116+
root_logger.handlers.clear()
117+
root_logger.setLevel(logging.NOTSET)
118+
119+
120+
# Initialize the root logger when the module is imported
121+
setup_root_logger()

ngraph/scenario.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def run(self) -> None:
5353
in scenario.results.
5454
"""
5555
for step in self.workflow:
56-
step.run(self)
56+
step.execute(self)
5757

5858
@classmethod
5959
def from_yaml(

0 commit comments

Comments
 (0)