Skip to content

Commit f84523b

Browse files
authored
Adding logging to SDK (#262)
* adding logging to runtime * Add comprehensive tests for BaseNode and StateManager functionality - Introduced a suite of tests for BaseNode, covering initialization, execution, and edge cases, ensuring robust validation of node behavior. - Added integration tests for Runtime and StateManager, validating state management, error handling, and interaction with external services. - Implemented tests for package imports and structure, ensuring all expected components are accessible and correctly configured. - Enhanced test coverage for concurrency scenarios, verifying the correct handling of multiple simultaneous executions and state triggers. - Improved overall test organization and clarity, contributing to a more maintainable and reliable codebase. * Refactor tests to suppress type checking warnings - Added `# type: ignore` comments to suppress type checking warnings in various test files, ensuring compatibility with type checkers while maintaining existing functionality. - Updated test cases across multiple modules, including BaseNode, StateManager, and integration tests, to enhance clarity and prevent type-related issues during testing. - Improved overall test robustness by ensuring that type hints do not interfere with test execution. * Bump version to 0.0.7b9 in _version.py * Add pytest-asyncio as a development dependency - Included `pytest-asyncio` in both `pyproject.toml` and `uv.lock` to support asynchronous testing. - Updated the version specification for `pytest-asyncio` to `>=0.24.0` to ensure compatibility with existing test frameworks. * Refactor integration tests to improve HTTP session mocking - Updated the integration tests in `test_integration.py` to use `exospherehost.runtime.ClientSession` for mocking HTTP responses, enhancing clarity and maintainability. - Simplified the mocking of HTTP responses by consolidating the setup process, ensuring consistent behavior across tests. - Fixed a bug in the runtime by adding `state_id` to the node mapping, improving the functionality of the state management system. * Enhance logging and testing configuration - Updated logging format in `runtime.py` to include the logger's name for better context in log messages. - Changed info log statements to debug level for state manager URI and API key fallback, reducing log verbosity. - Added pytest configuration options in `pyproject.toml` to specify asyncio mode and test paths, improving test execution setup.
1 parent a813edd commit f84523b

11 files changed

Lines changed: 2191 additions & 10 deletions
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version = "0.0.7b8"
1+
version = "0.0.7b9"

python-sdk/exospherehost/runtime.py

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,46 @@
11
import asyncio
22
import os
3+
import logging
4+
import traceback
5+
36
from asyncio import Queue, sleep
47
from typing import List, Dict
5-
68
from pydantic import BaseModel
79
from .node.BaseNode import BaseNode
810
from aiohttp import ClientSession
9-
from logging import getLogger
1011

11-
logger = getLogger(__name__)
12+
logger = logging.getLogger(__name__)
13+
14+
def _setup_default_logging():
15+
"""
16+
Setup default logging only if no handlers are configured.
17+
Respects user's existing logging configuration.
18+
"""
19+
root_logger = logging.getLogger()
20+
21+
# Don't interfere if user has already configured logging
22+
if root_logger.handlers:
23+
return
24+
25+
# Allow users to disable default logging
26+
if os.environ.get('EXOSPHERE_DISABLE_DEFAULT_LOGGING'):
27+
return
28+
29+
# Get log level from environment or default to INFO
30+
log_level_name = os.environ.get('EXOSPHERE_LOG_LEVEL', 'INFO').upper()
31+
log_level = getattr(logging, log_level_name, logging.INFO)
32+
33+
# Setup basic configuration with clean formatting
34+
logging.basicConfig(
35+
level=log_level,
36+
format='%(asctime)s | %(levelname)s | %(name)s | %(message)s',
37+
datefmt='%Y-%m-%d %H:%M:%S'
38+
)
39+
40+
# Log that we're using default configuration
41+
logger = logging.getLogger(__name__)
42+
logger.debug(f"ExosphereHost: Using default logging configuration (level: {log_level_name})")
43+
1244

1345
class Runtime:
1446
"""
@@ -48,6 +80,9 @@ class Runtime:
4880
"""
4981

5082
def __init__(self, namespace: str, name: str, nodes: List[type[BaseNode]], state_manager_uri: str | None = None, key: str | None = None, batch_size: int = 16, workers: int = 4, state_manage_version: str = "v0", poll_interval: int = 1):
83+
84+
_setup_default_logging()
85+
5186
self._name = name
5287
self._namespace = namespace
5388
self._key = key
@@ -72,8 +107,10 @@ def _set_config_from_env(self):
72107
Set configuration from environment variables if not provided.
73108
"""
74109
if self._state_manager_uri is None:
110+
logger.debug("State manager URI not provided, falling back to environment variable EXOSPHERE_STATE_MANAGER_URI")
75111
self._state_manager_uri = os.environ.get("EXOSPHERE_STATE_MANAGER_URI")
76112
if self._key is None:
113+
logger.debug("API key not provided, falling back to environment variable EXOSPHERE_API_KEY")
77114
self._key = os.environ.get("EXOSPHERE_API_KEY")
78115

79116
def _validate_runtime(self):
@@ -130,6 +167,7 @@ async def _register(self):
130167
Raises:
131168
RuntimeError: If registration fails.
132169
"""
170+
logger.info(f"Registering nodes: {[f"{self._namespace}/{node.__name__}" for node in self._nodes]}")
133171
async with ClientSession() as session:
134172
endpoint = self._get_register_endpoint()
135173
body = {
@@ -153,8 +191,10 @@ async def _register(self):
153191
res = await response.json()
154192

155193
if response.status != 200:
194+
logger.error(f"Failed to register nodes: {res}")
156195
raise RuntimeError(f"Failed to register nodes: {res}")
157196

197+
logger.info(f"Registered nodes: {[f"{self._namespace}/{node.__name__}" for node in self._nodes]}")
158198
return res
159199

160200
async def _enqueue_call(self):
@@ -174,6 +214,7 @@ async def _enqueue_call(self):
174214

175215
if response.status != 200:
176216
logger.error(f"Failed to enqueue states: {res}")
217+
raise RuntimeError(f"Failed to enqueue states: {res}")
177218

178219
return res
179220

@@ -189,9 +230,12 @@ async def _enqueue(self):
189230
data = await self._enqueue_call()
190231
for state in data.get("states", []):
191232
await self._state_queue.put(state)
233+
logger.info(f"Enqueued states: {len(data.get('states', []))}")
192234
except Exception as e:
193235
logger.error(f"Error enqueuing states: {e}")
194-
236+
await sleep(self._poll_interval * 2)
237+
continue
238+
195239
await sleep(self._poll_interval)
196240

197241
async def _notify_executed(self, state_id: str, outputs: List[BaseNode.Outputs]):
@@ -212,6 +256,7 @@ async def _notify_executed(self, state_id: str, outputs: List[BaseNode.Outputs])
212256

213257
if response.status != 200:
214258
logger.error(f"Failed to notify executed state {state_id}: {res}")
259+
215260

216261
async def _notify_errored(self, state_id: str, error: str):
217262
"""
@@ -232,6 +277,7 @@ async def _notify_errored(self, state_id: str, error: str):
232277
if response.status != 200:
233278
logger.error(f"Failed to notify errored state {state_id}: {res}")
234279

280+
235281
async def _get_secrets(self, state_id: str) -> Dict[str, str]:
236282
"""
237283
Get secrets for a state.
@@ -306,31 +352,44 @@ def _validate_nodes(self):
306352
if len(errors) > 0:
307353
raise ValueError("Following errors while validating nodes: " + "\n".join(errors))
308354

309-
async def _worker(self):
355+
async def _worker(self, idx: int):
310356
"""
311357
Worker task that processes states from the queue.
312358
313359
Continuously fetches states from the queue, executes the corresponding node,
314360
and notifies the state manager of the result.
315361
"""
362+
logger.info(f"Starting worker thread {idx} for nodes: {[f"{self._namespace}/{node.__name__}" for node in self._nodes]}")
363+
316364
while True:
317365
state = await self._state_queue.get()
366+
node = None
318367

319368
try:
320369
node = self._node_mapping[state["node_name"]]
370+
logger.info(f"Executing state {state['state_id']} for node {node.__name__}")
371+
321372
secrets = await self._get_secrets(state["state_id"])
322-
outputs = await node()._execute(node.Inputs(**state["inputs"]), node.Secrets(**secrets["secrets"]))
373+
logger.info(f"Got secrets for state {state['state_id']} for node {node.__name__}")
323374

375+
outputs = await node()._execute(node.Inputs(**state["inputs"]), node.Secrets(**secrets["secrets"])) # type: ignore
376+
logger.info(f"Got outputs for state {state['state_id']} for node {node.__name__}")
377+
324378
if outputs is None:
325379
outputs = []
326380

327381
if not isinstance(outputs, list):
328382
outputs = [outputs]
329383

330384
await self._notify_executed(state["state_id"], outputs)
385+
logger.info(f"Notified executed state {state['state_id']} for node {node.__name__ if node else "unknown"}")
331386

332387
except Exception as e:
388+
logger.error(f"Error executing state {state['state_id']} for node {node.__name__ if node else "unknown"}: {e}")
389+
logger.error(traceback.format_exc())
390+
333391
await self._notify_errored(state["state_id"], str(e))
392+
logger.info(f"Notified errored state {state['state_id']} for node {node.__name__ if node else "unknown"}")
334393

335394
self._state_queue.task_done() # type: ignore
336395

@@ -346,7 +405,7 @@ async def _start(self):
346405
await self._register()
347406

348407
poller = asyncio.create_task(self._enqueue())
349-
worker_tasks = [asyncio.create_task(self._worker()) for _ in range(self._workers)]
408+
worker_tasks = [asyncio.create_task(self._worker(idx)) for idx in range(self._workers)]
350409

351410
await asyncio.gather(poller, *worker_tasks)
352411

python-sdk/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,10 @@ dev = [
3333
"ruff>=0.12.5",
3434
"pytest>=8.3.0",
3535
"pytest-cov>=5.0.0",
36+
"pytest-asyncio>=0.24.0",
3637
]
38+
39+
[tool.pytest.ini_options]
40+
asyncio_mode = "auto"
41+
addopts = "-ra"
42+
testpaths = ["tests"]

0 commit comments

Comments
 (0)