golem-vm-provider 0.1.26__py3-none-any.whl → 0.1.28__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,90 +1,71 @@
1
1
  import aiohttp
2
2
  import asyncio
3
3
  import logging
4
- import psutil
5
- from datetime import datetime
6
- from typing import Dict, Optional
4
+ from abc import ABC, abstractmethod
5
+ from typing import Optional
7
6
 
8
7
  from ..config import settings
9
8
  from ..utils.retry import async_retry
9
+ from .resource_tracker import ResourceTracker
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
- class ResourceMonitor:
14
- """Monitor system resources."""
15
-
16
- @staticmethod
17
- def get_cpu_count() -> int:
18
- """Get number of CPU cores."""
19
- return psutil.cpu_count()
20
-
21
- @staticmethod
22
- def get_memory_gb() -> int:
23
- """Get available memory in GB."""
24
- return psutil.virtual_memory().available // (1024 ** 3)
25
-
26
- @staticmethod
27
- def get_storage_gb() -> int:
28
- """Get available storage in GB."""
29
- return psutil.disk_usage("/").free // (1024 ** 3)
30
-
31
- @staticmethod
32
- def get_cpu_percent() -> float:
33
- """Get CPU usage percentage."""
34
- return psutil.cpu_percent(interval=1)
35
-
36
- @staticmethod
37
- def get_memory_percent() -> float:
38
- """Get memory usage percentage."""
39
- return psutil.virtual_memory().percent
40
-
41
- @staticmethod
42
- def get_storage_percent() -> float:
43
- """Get storage usage percentage."""
44
- return psutil.disk_usage("/").percent
45
-
46
- class ResourceAdvertiser:
47
- """Advertise available resources to discovery service."""
13
+ class Advertiser(ABC):
14
+ """Abstract base class for advertisers."""
15
+
16
+ @abstractmethod
17
+ async def initialize(self):
18
+ """Initialize the advertiser."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ async def start_loop(self):
23
+ """Start the advertising loop."""
24
+ pass
25
+
26
+ @abstractmethod
27
+ async def stop(self):
28
+ """Stop the advertising loop."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ async def post_advertisement(self):
33
+ """Post a single advertisement."""
34
+ pass
35
+
36
+ class DiscoveryServerAdvertiser(Advertiser):
37
+ """Advertise available resources to a discovery service."""
48
38
 
49
39
  def __init__(
50
40
  self,
51
41
  resource_tracker: 'ResourceTracker',
52
42
  discovery_url: Optional[str] = None,
53
43
  provider_id: Optional[str] = None,
54
- update_interval: Optional[int] = None
55
44
  ):
56
45
  self.resource_tracker = resource_tracker
57
46
  self.discovery_url = discovery_url or settings.DISCOVERY_URL
58
47
  self.provider_id = provider_id or settings.PROVIDER_ID
59
- self.update_interval = update_interval or settings.ADVERTISEMENT_INTERVAL
60
48
  self.session: Optional[aiohttp.ClientSession] = None
61
49
  self._stop_event = asyncio.Event()
62
50
 
63
- async def start(self):
64
- """Start advertising resources."""
51
+ async def initialize(self):
52
+ """Initialize the advertiser."""
65
53
  self.session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
66
- # Register for resource updates
67
- self.resource_tracker.on_update(self._post_advertisement)
68
-
69
- # Test discovery service connection with retries
54
+ self.resource_tracker.on_update(
55
+ lambda: asyncio.create_task(self.post_advertisement())
56
+ )
70
57
  try:
71
58
  await self._check_discovery_health()
72
59
  except Exception as e:
73
60
  logger.warning(f"Could not connect to discovery service after retries, continuing without advertising: {e}")
74
61
  return
75
-
62
+
63
+ async def start_loop(self):
64
+ """Start advertising resources in a loop."""
76
65
  try:
77
66
  while not self._stop_event.is_set():
78
- try:
79
- await self._post_advertisement()
80
- except aiohttp.ClientError as e:
81
- logger.error(f"Network error posting advertisement: {e}")
82
- await asyncio.sleep(min(60, self.update_interval))
83
- except Exception as e:
84
- logger.error(f"Failed to post advertisement: {e}")
85
- await asyncio.sleep(min(60, self.update_interval))
86
- else:
87
- await asyncio.sleep(self.update_interval)
67
+ await self.post_advertisement()
68
+ await asyncio.sleep(settings.ADVERTISEMENT_INTERVAL)
88
69
  finally:
89
70
  await self.stop()
90
71
 
@@ -106,19 +87,17 @@ class ResourceAdvertiser:
106
87
  raise Exception(f"Discovery service health check failed: {response.status}")
107
88
 
108
89
  @async_retry(retries=3, delay=1.0, backoff=2.0, exceptions=(aiohttp.ClientError, asyncio.TimeoutError))
109
- async def _post_advertisement(self):
90
+ async def post_advertisement(self):
110
91
  """Post resource advertisement to discovery service."""
111
92
  if not self.session:
112
93
  raise RuntimeError("Session not initialized")
113
94
 
114
95
  resources = self.resource_tracker.get_available_resources()
115
96
 
116
- # Don't advertise if resources are too low
117
97
  if not self.resource_tracker._meets_minimum_requirements(resources):
118
98
  logger.warning("Resources too low, skipping advertisement")
119
99
  return
120
100
 
121
- # Get public IP with retries
122
101
  try:
123
102
  ip_address = await self._get_public_ip()
124
103
  except Exception as e:
@@ -130,7 +109,7 @@ class ResourceAdvertiser:
130
109
  f"{self.discovery_url}/api/v1/advertisements",
131
110
  headers={
132
111
  "X-Provider-ID": self.provider_id,
133
- "X-Provider-Signature": "signature", # TODO: Implement signing
112
+ "X-Provider-Signature": "signature",
134
113
  "Content-Type": "application/json"
135
114
  },
136
115
  json={
@@ -138,7 +117,7 @@ class ResourceAdvertiser:
138
117
  "country": settings.PROVIDER_COUNTRY,
139
118
  "resources": resources
140
119
  },
141
- timeout=aiohttp.ClientTimeout(total=5) # 5 second timeout for advertisement
120
+ timeout=aiohttp.ClientTimeout(total=5)
142
121
  ) as response:
143
122
  if not response.ok:
144
123
  error_text = await response.text()
@@ -159,7 +138,6 @@ class ResourceAdvertiser:
159
138
  if not self.session:
160
139
  raise RuntimeError("Session not initialized")
161
140
 
162
- # Try multiple IP services in case one fails
163
141
  services = [
164
142
  "https://api.ipify.org",
165
143
  "https://ifconfig.me/ip",
@@ -2,6 +2,7 @@ import asyncio
2
2
  from typing import Optional
3
3
 
4
4
  from golem_base_sdk import GolemBaseClient, GolemBaseCreate, GolemBaseUpdate, GolemBaseDelete, Annotation
5
+ from .advertiser import Advertiser
5
6
  from .golem_base_utils import get_provider_entity_keys
6
7
  from ..config import settings
7
8
  from ..utils.logging import setup_logger
@@ -9,7 +10,7 @@ from ..utils.logging import setup_logger
9
10
  logger = setup_logger(__name__)
10
11
 
11
12
 
12
- class GolemBaseAdvertiser:
13
+ class GolemBaseAdvertiser(Advertiser):
13
14
  """Advertise available resources to the Golem Base network."""
14
15
 
15
16
  def __init__(self, resource_tracker: "ResourceTracker"):
@@ -89,11 +90,16 @@ class GolemBaseAdvertiser:
89
90
  current_annotations = {ann.key: ann.value for ann in metadata.numeric_annotations}
90
91
  current_annotations.update({ann.key: ann.value for ann in metadata.string_annotations})
91
92
 
92
- if (current_annotations.get("golem_cpu") == resources["cpu"] and
93
- current_annotations.get("golem_memory") == resources["memory"] and
94
- current_annotations.get("golem_storage") == resources["storage"] and
95
- current_annotations.get("golem_ip_address") == ip_address and
96
- current_annotations.get("golem_provider_name") == settings.PROVIDER_NAME):
93
+ # Full comparison of all annotations
94
+ expected_annotations = {ann.key: ann.value for ann in string_annotations}
95
+ expected_annotations.update({ann.key: ann.value for ann in numeric_annotations})
96
+
97
+ # Debugging logs to compare annotations
98
+ logger.info(f"IP address from settings: {ip_address}")
99
+ logger.info(f"Current on-chain annotations: {current_annotations}")
100
+ logger.info(f"Expected annotations based on current config: {expected_annotations}")
101
+
102
+ if sorted(current_annotations.items()) == sorted(expected_annotations.items()):
97
103
  logger.info("Advertisement is up-to-date. Waiting for expiration.")
98
104
  else:
99
105
  logger.info("Advertisement is outdated. Updating.")
@@ -0,0 +1,34 @@
1
+ import psutil
2
+
3
+ class ResourceMonitor:
4
+ """Monitor system resources."""
5
+
6
+ @staticmethod
7
+ def get_cpu_count() -> int:
8
+ """Get number of CPU cores."""
9
+ return psutil.cpu_count()
10
+
11
+ @staticmethod
12
+ def get_memory_gb() -> int:
13
+ """Get available memory in GB."""
14
+ return psutil.virtual_memory().available // (1024 ** 3)
15
+
16
+ @staticmethod
17
+ def get_storage_gb() -> int:
18
+ """Get available storage in GB."""
19
+ return psutil.disk_usage("/").free // (1024 ** 3)
20
+
21
+ @staticmethod
22
+ def get_cpu_percent() -> float:
23
+ """Get CPU usage percentage."""
24
+ return psutil.cpu_percent(interval=1)
25
+
26
+ @staticmethod
27
+ def get_memory_percent() -> float:
28
+ """Get memory usage percentage."""
29
+ return psutil.virtual_memory().percent
30
+
31
+ @staticmethod
32
+ def get_storage_percent() -> float:
33
+ """Get storage usage percentage."""
34
+ return psutil.disk_usage("/").percent
@@ -11,7 +11,7 @@ class ResourceTracker:
11
11
 
12
12
  def __init__(self):
13
13
  """Initialize resource tracker."""
14
- from .advertiser import ResourceMonitor
14
+ from .resource_monitor import ResourceMonitor
15
15
  self.total_resources = {
16
16
  "cpu": ResourceMonitor.get_cpu_count(),
17
17
  "memory": ResourceMonitor.get_memory_gb(),
@@ -0,0 +1,24 @@
1
+ import asyncio
2
+ from typing import Optional
3
+
4
+ from .advertiser import Advertiser
5
+ from ..config import settings
6
+
7
+ class AdvertisementService:
8
+ """Service for managing the advertisement lifecycle."""
9
+
10
+ def __init__(self, advertiser: Advertiser):
11
+ self.advertiser = advertiser
12
+ self._task: Optional[asyncio.Task] = None
13
+
14
+ async def start(self):
15
+ """Initialize and start the advertiser."""
16
+ await self.advertiser.initialize()
17
+ self._task = asyncio.create_task(self.advertiser.start_loop())
18
+
19
+ async def stop(self):
20
+ """Stop the advertiser."""
21
+ if self._task:
22
+ self._task.cancel()
23
+ await self._task
24
+ await self.advertiser.stop()
provider/main.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from .api import routes
2
2
  import asyncio
3
3
  import os
4
+ import socket
4
5
  from fastapi import FastAPI
5
6
  from fastapi.middleware.cors import CORSMiddleware
6
7
  from typing import Optional
@@ -9,15 +10,35 @@ from .config import settings
9
10
  from .utils.logging import setup_logger, PROCESS, SUCCESS
10
11
  from .utils.ascii_art import startup_animation
11
12
  from .discovery.resource_tracker import ResourceTracker
12
- from .discovery.advertiser import ResourceAdvertiser
13
- from .discovery.golem_base_advertiser import GolemBaseAdvertiser
14
- from .vm.multipass import MultipassProvider
15
- from .vm.port_manager import PortManager
16
- from .security.faucet import FaucetClient
17
-
18
- logger = setup_logger(__name__)
13
+ from .discovery.advertiser import DiscoveryServerAdvertiser
14
+ from .container import Container
15
+ from .service import ProviderService
16
+ from .utils.logging import setup_logger
19
17
 
20
18
  app = FastAPI(title="VM on Golem Provider")
19
+ container = Container()
20
+ container.config.from_pydantic(settings)
21
+ app.container = container
22
+ container.wire(modules=[".api.routes"])
23
+
24
+ from .vm.models import VMNotFoundError
25
+ from fastapi import Request
26
+ from fastapi.responses import JSONResponse
27
+
28
+ @app.exception_handler(VMNotFoundError)
29
+ async def vm_not_found_exception_handler(request: Request, exc: VMNotFoundError):
30
+ return JSONResponse(
31
+ status_code=404,
32
+ content={"message": str(exc)},
33
+ )
34
+
35
+ @app.exception_handler(Exception)
36
+ async def generic_exception_handler(request: Request, exc: Exception):
37
+ return JSONResponse(
38
+ status_code=500,
39
+ content={"message": "An unexpected error occurred"},
40
+ )
41
+
21
42
  # Add CORS middleware
22
43
  app.add_middleware(
23
44
  CORSMiddleware,
@@ -27,156 +48,18 @@ app.add_middleware(
27
48
  allow_headers=["*"], # Allows all headers
28
49
  )
29
50
 
30
-
31
- async def setup_provider() -> None:
32
- """Setup and initialize the provider components."""
33
- try:
34
- # Create resource tracker first
35
- logger.process("🔄 Initializing resource tracker...")
36
- resource_tracker = ResourceTracker()
37
- app.state.resource_tracker = resource_tracker
38
-
39
- # Create provider with resource tracker and temporary port manager
40
- logger.process("🔄 Initializing VM provider...")
41
- provider = MultipassProvider(
42
- resource_tracker, port_manager=None) # Will be set later
43
-
44
- try:
45
- # Initialize provider (without port operations)
46
- await asyncio.wait_for(provider.initialize(), timeout=30)
47
-
48
- # Store provider reference
49
- app.state.provider = provider
50
- app.state.proxy_manager = provider.proxy_manager
51
-
52
- # Initialize port manager first to verify all ports
53
- logger.process("🔄 Initializing port manager...")
54
- port_manager = PortManager(
55
- start_port=settings.PORT_RANGE_START,
56
- end_port=settings.PORT_RANGE_END,
57
- discovery_port=settings.PORT,
58
- skip_verification=settings.SKIP_PORT_VERIFICATION
59
- )
60
-
61
- if not await port_manager.initialize():
62
- raise RuntimeError("Port verification failed")
63
-
64
- # Store port manager references
65
- app.state.port_manager = port_manager
66
- provider.port_manager = port_manager
67
- app.state.proxy_manager.port_manager = port_manager
68
-
69
- # Now restore proxy configurations using only verified ports
70
- logger.process("🔄 Restoring proxy configurations...")
71
- await app.state.proxy_manager._load_state()
72
-
73
- except asyncio.TimeoutError:
74
- logger.error("Provider initialization timed out")
75
- raise
76
- except Exception as e:
77
- logger.error(f"Failed to initialize provider: {e}")
78
- raise
79
-
80
- # Create advertiser
81
- logger.process("🔄 Initializing resource advertiser...")
82
- if settings.DISCOVERY_DRIVER == "golem-base":
83
- advertiser = GolemBaseAdvertiser(
84
- resource_tracker=resource_tracker
85
- )
86
- await advertiser.initialize()
87
- else:
88
- advertiser = ResourceAdvertiser(
89
- resource_tracker=resource_tracker,
90
- discovery_url=settings.DISCOVERY_URL,
91
- provider_id=settings.PROVIDER_ID
92
- )
93
- app.state.advertiser = advertiser
94
-
95
- logger.success(
96
- "✨ Provider setup complete and ready to accept requests")
97
- except Exception as e:
98
- logger.error(f"Failed to setup provider: {e}")
99
- # Attempt cleanup of any initialized components
100
- await cleanup_provider()
101
- raise
102
-
103
-
104
- async def cleanup_provider() -> None:
105
- """Cleanup provider components."""
106
- cleanup_errors = []
107
-
108
- # Stop advertiser
109
- if hasattr(app.state, "advertiser"):
110
- try:
111
- await app.state.advertiser.stop()
112
- if hasattr(app.state, "advertiser_task"):
113
- app.state.advertiser_task.cancel()
114
- try:
115
- await app.state.advertiser_task
116
- except asyncio.CancelledError:
117
- pass
118
- except Exception as e:
119
- cleanup_errors.append(f"Failed to stop advertiser: {e}")
120
-
121
- # Cleanup proxy manager first to stop all proxy servers
122
- if hasattr(app.state, "proxy_manager"):
123
- try:
124
- await asyncio.wait_for(app.state.proxy_manager.cleanup(), timeout=30)
125
- except asyncio.TimeoutError:
126
- cleanup_errors.append("Proxy manager cleanup timed out")
127
- except Exception as e:
128
- cleanup_errors.append(f"Failed to cleanup proxy manager: {e}")
129
-
130
- # Cleanup provider
131
- if hasattr(app.state, "provider"):
132
- try:
133
- await asyncio.wait_for(app.state.provider.cleanup(), timeout=30)
134
- except asyncio.TimeoutError:
135
- cleanup_errors.append("Provider cleanup timed out")
136
- except Exception as e:
137
- cleanup_errors.append(f"Failed to cleanup provider: {e}")
138
-
139
- if cleanup_errors:
140
- error_msg = "\n".join(cleanup_errors)
141
- logger.error(f"Errors during cleanup:\n{error_msg}")
142
- else:
143
- logger.success("✨ Provider cleanup complete")
144
-
145
-
146
51
  @app.on_event("startup")
147
52
  async def startup_event():
148
53
  """Handle application startup."""
149
- try:
150
- # Display startup animation
151
- await startup_animation()
152
-
153
- # Initialize provider
154
- await setup_provider()
155
-
156
- # Check wallet balance and request funds if needed
157
- faucet_client = FaucetClient(
158
- faucet_url=settings.FAUCET_URL,
159
- captcha_url=settings.CAPTCHA_URL,
160
- captcha_api_key=settings.CAPTCHA_API_KEY,
161
- )
162
- await faucet_client.get_funds(settings.PROVIDER_ID)
163
-
164
- # Post initial advertisement and start advertising loop
165
- if isinstance(app.state.advertiser, GolemBaseAdvertiser):
166
- await app.state.advertiser.post_advertisement()
167
- app.state.advertiser_task = asyncio.create_task(app.state.advertiser.start_loop())
168
-
169
- except Exception as e:
170
- logger.error(f"Startup failed: {e}")
171
- # Ensure proper cleanup
172
- await cleanup_provider()
173
- raise
54
+ provider_service = container.provider_service()
55
+ await provider_service.setup(app)
174
56
 
175
57
 
176
58
  @app.on_event("shutdown")
177
59
  async def shutdown_event():
178
60
  """Handle application shutdown."""
179
- await cleanup_provider()
61
+ provider_service = container.provider_service()
62
+ await provider_service.cleanup()
180
63
 
181
64
  # Import routes after app creation to avoid circular imports
182
65
  app.include_router(routes.router, prefix="/api/v1")
@@ -225,6 +108,9 @@ async def verify_provider_port(port: int) -> bool:
225
108
  return False
226
109
 
227
110
 
111
+ # The get_local_ip function has been removed as this logic is now handled in config.py
112
+
113
+
228
114
  import typer
229
115
 
230
116
  cli = typer.Typer()
@@ -232,21 +118,36 @@ cli = typer.Typer()
232
118
  @cli.command()
233
119
  def start(no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification.")):
234
120
  """Start the provider server."""
121
+ run_server(dev_mode=False, no_verify_port=no_verify_port)
122
+
123
+ @cli.command()
124
+ def dev(no_verify_port: bool = typer.Option(True, "--no-verify-port", help="Skip provider port verification.")):
125
+ """Start the provider server in development mode."""
126
+ run_server(dev_mode=True, no_verify_port=no_verify_port)
127
+
128
+ def run_server(dev_mode: bool, no_verify_port: bool):
129
+ """Helper to run the uvicorn server."""
235
130
  import sys
236
131
  from pathlib import Path
237
132
  from dotenv import load_dotenv
238
133
  import uvicorn
239
134
  from .utils.logging import setup_logger
135
+
136
+ # Load appropriate .env file
137
+ env_file = ".env.dev" if dev_mode else ".env"
138
+ env_path = Path(__file__).parent.parent / env_file
139
+ load_dotenv(dotenv_path=env_path)
140
+
141
+ # The logic for setting the public IP in dev mode is now handled in config.py
142
+ # The following lines are no longer needed and have been removed.
143
+
144
+ # Import settings after loading env
240
145
  from .config import settings
241
146
 
242
147
  # Configure logging with debug mode
243
- logger = setup_logger(__name__, debug=True)
148
+ logger = setup_logger(__name__, debug=dev_mode)
244
149
 
245
150
  try:
246
- # Load environment variables from .env file
247
- env_path = Path(__file__).parent.parent / '.env'
248
- load_dotenv(dotenv_path=env_path)
249
-
250
151
  # Log environment variables
251
152
  logger.info("Environment variables:")
252
153
  for key, value in os.environ.items():
@@ -275,7 +176,7 @@ def start(no_verify_port: bool = typer.Option(False, "--no-verify-port", help="S
275
176
  host=settings.HOST,
276
177
  port=settings.PORT,
277
178
  reload=settings.DEBUG,
278
- log_level="info" if not settings.DEBUG else "debug",
179
+ log_level="debug" if dev_mode else "info",
279
180
  log_config=log_config,
280
181
  timeout_keep_alive=60, # Increase keep-alive timeout
281
182
  limit_concurrency=100, # Limit concurrent connections
provider/service.py ADDED
@@ -0,0 +1,67 @@
1
+ import asyncio
2
+ from fastapi import FastAPI
3
+
4
+ from .utils.logging import setup_logger
5
+ from .vm.service import VMService
6
+ from .discovery.service import AdvertisementService
7
+
8
+ logger = setup_logger(__name__)
9
+
10
+
11
+ class ProviderService:
12
+ """Service for managing the provider's lifecycle."""
13
+
14
+ def __init__(self, vm_service: VMService, advertisement_service: AdvertisementService, port_manager):
15
+ self.vm_service = vm_service
16
+ self.advertisement_service = advertisement_service
17
+ self.port_manager = port_manager
18
+
19
+ async def setup(self, app: FastAPI):
20
+ """Setup and initialize the provider components."""
21
+ from .config import settings
22
+ from .utils.ascii_art import startup_animation
23
+ from .security.faucet import FaucetClient
24
+
25
+ try:
26
+ # Display startup animation
27
+ await startup_animation()
28
+
29
+ logger.process("🔄 Initializing provider...")
30
+
31
+ # Setup directories
32
+ self._setup_directories()
33
+
34
+ # Initialize services
35
+ await self.port_manager.initialize()
36
+ await self.vm_service.provider.initialize()
37
+ await self.advertisement_service.start()
38
+
39
+ # Check wallet balance and request funds if needed
40
+ faucet_client = FaucetClient(
41
+ faucet_url=settings.FAUCET_URL,
42
+ captcha_url=settings.CAPTCHA_URL,
43
+ captcha_api_key=settings.CAPTCHA_API_KEY,
44
+ )
45
+ await faucet_client.get_funds(settings.PROVIDER_ID)
46
+
47
+ logger.success("✨ Provider setup complete")
48
+ except Exception as e:
49
+ logger.error(f"Startup failed: {e}")
50
+ await self.cleanup()
51
+ raise
52
+
53
+ async def cleanup(self):
54
+ """Cleanup provider components."""
55
+ logger.process("🔄 Cleaning up provider...")
56
+ await self.advertisement_service.stop()
57
+ await self.vm_service.provider.cleanup()
58
+ logger.success("✨ Provider cleanup complete")
59
+
60
+ def _setup_directories(self):
61
+ """Create necessary directories for the provider."""
62
+ from .config import settings
63
+ from pathlib import Path
64
+
65
+ Path(settings.VM_DATA_DIR).mkdir(parents=True, exist_ok=True)
66
+ Path(settings.SSH_KEY_DIR).mkdir(parents=True, exist_ok=True)
67
+ Path(settings.CLOUD_INIT_DIR).mkdir(parents=True, exist_ok=True)
File without changes
provider/utils/logging.py CHANGED
@@ -39,42 +39,26 @@ def setup_logger(name: Optional[str] = None, debug: bool = False) -> logging.Log
39
39
  Configured logger instance
40
40
  """
41
41
  logger = logging.getLogger(name or __name__)
42
- logger.handlers = [] # Clear existing handlers
43
-
44
- # Fancy handler for important logs
45
- fancy_handler = colorlog.StreamHandler(sys.stdout)
46
- fancy_formatter = colorlog.ColoredFormatter(
47
- "%(log_color)s[%(asctime)s] %(message)s",
42
+ if logger.handlers:
43
+ return logger # Already configured
44
+
45
+ handler = colorlog.StreamHandler(sys.stdout)
46
+ formatter = colorlog.ColoredFormatter(
47
+ "%(log_color)s[%(asctime)s] %(levelname)s: %(message)s",
48
48
  datefmt="%Y-%m-%d %H:%M:%S",
49
- reset=True,
50
49
  log_colors={
50
+ 'DEBUG': 'cyan',
51
51
  'INFO': 'green',
52
52
  'PROCESS': 'yellow',
53
53
  'WARNING': 'yellow',
54
54
  'SUCCESS': 'green,bold',
55
55
  'ERROR': 'red',
56
56
  'CRITICAL': 'red,bold',
57
- },
58
- secondary_log_colors={},
59
- style='%'
57
+ }
60
58
  )
61
- fancy_handler.setFormatter(fancy_formatter)
62
- fancy_handler.addFilter(lambda record: record.levelno in [INFO, PROCESS, SUCCESS, WARNING, ERROR, CRITICAL])
63
- logger.addHandler(fancy_handler)
64
-
65
- if debug:
66
- # Debug handler for detailed logs
67
- debug_handler = logging.StreamHandler(sys.stdout)
68
- debug_formatter = logging.Formatter(
69
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
70
- datefmt="%Y-%m-%d %H:%M:%S"
71
- )
72
- debug_handler.setFormatter(debug_formatter)
73
- debug_handler.addFilter(lambda record: record.levelno == DEBUG)
74
- logger.addHandler(debug_handler)
75
- logger.setLevel(logging.DEBUG)
76
- else:
77
- logger.setLevel(logging.INFO)
59
+ handler.setFormatter(formatter)
60
+ logger.addHandler(handler)
61
+ logger.setLevel(logging.DEBUG if debug else logging.INFO)
78
62
 
79
63
  return logger
80
64