Skip to content

Commit ca2b038

Browse files
codebydivineclaude
andcommitted
feat: add structured logging with structlog
- Add structlog 25.4.0 dependency for consistent structured logging - Create new logger.py module with JSON output configuration - Add contextual logging to BaseTokenAPI for API operations - Include structured logging for health checks, version, and network queries - Provide meaningful context with key-value pairs for better debugging 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 055343c commit ca2b038

File tree

4 files changed

+56
-1
lines changed

4 files changed

+56
-1
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies = [
1414
"divine-typed-requests>=0.1.32",
1515
"divine-type-enforcer>=0.1.13",
1616
"httpx[http2]>=0.28.1",
17+
"structlog>=25.4.0",
1718
]
1819

1920
[project.urls]

src/thegraph_token_api/base.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
# Import divine-typed-requests (should be installed as a package)
1111
from typed_requests import NetworkingManager
1212

13+
from .logger import get_logger
1314
from .types import NetworksResponse, VersionResponse
1415

16+
logger = get_logger(__name__)
17+
1518

1619
class BaseTokenAPI:
1720
"""
@@ -32,10 +35,13 @@ def __init__(self, api_key: str | None = None, base_url: str | None = None):
3235
self.base_url = base_url or os.getenv("THEGRAPH_API_ENDPOINT", "https://token-api.thegraph.com")
3336

3437
if not self.api_key:
38+
logger.error("API key is required but not provided")
3539
raise ValueError(
3640
"API key is required. Provide it via api_key parameter or THEGRAPH_API_KEY environment variable."
3741
)
3842

43+
logger.info("Initializing TheGraph Token API client", base_url=self.base_url)
44+
3945
self.manager = NetworkingManager()
4046
self._headers = {
4147
"Authorization": f"Bearer {self.api_key}",
@@ -69,11 +75,13 @@ def _add_optional_params(self, params: dict[str, Any], **kwargs: Any) -> dict[st
6975

7076
async def __aenter__(self) -> "BaseTokenAPI":
7177
"""Async context manager entry."""
78+
logger.debug("Starting networking manager")
7279
await self.manager.startup()
7380
return self
7481

7582
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
7683
"""Async context manager exit."""
84+
logger.debug("Shutting down networking manager")
7785
await self.manager.shutdown()
7886

7987
# ===== Monitoring Methods =====
@@ -85,8 +93,11 @@ async def get_health(self) -> str:
8593
Returns:
8694
Health status string (should be "OK")
8795
"""
96+
logger.debug("Checking API health")
8897
response = await self.manager.get(f"{self.base_url}/health", headers=self._headers, timeout=30)
89-
return str(response.text)
98+
health_status = str(response.text)
99+
logger.info("Health check completed", status=health_status)
100+
return health_status
90101

91102
async def get_version(self) -> VersionResponse:
92103
"""
@@ -95,9 +106,11 @@ async def get_version(self) -> VersionResponse:
95106
Returns:
96107
VersionResponse with version details
97108
"""
109+
logger.debug("Fetching API version")
98110
response = await self.manager.get(
99111
f"{self.base_url}/version", headers=self._headers, expected_type=VersionResponse, timeout=30
100112
)
113+
logger.info("Version information retrieved", version=response.data.get("version", "unknown"))
101114
return response.data
102115

103116
async def get_networks(self) -> NetworksResponse:
@@ -107,7 +120,10 @@ async def get_networks(self) -> NetworksResponse:
107120
Returns:
108121
NetworksResponse with supported network information
109122
"""
123+
logger.debug("Fetching supported networks")
110124
response = await self.manager.get(
111125
f"{self.base_url}/networks", headers=self._headers, expected_type=NetworksResponse, timeout=30
112126
)
127+
networks_count = len(response.data.get("networks", []))
128+
logger.info("Networks information retrieved", networks_count=networks_count)
113129
return response.data

src/thegraph_token_api/logger.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import structlog
2+
from structlog.stdlib import BoundLogger
3+
4+
5+
def get_logger(name: str) -> BoundLogger:
6+
"""Get a structured logger for a specific module."""
7+
return structlog.get_logger(name)
8+
9+
10+
# Configure structlog for JSON output with consistent formatting
11+
structlog.configure(
12+
processors=[
13+
structlog.stdlib.filter_by_level,
14+
structlog.stdlib.add_logger_name,
15+
structlog.stdlib.add_log_level,
16+
structlog.stdlib.PositionalArgumentsFormatter(),
17+
structlog.processors.TimeStamper(fmt="iso"),
18+
structlog.processors.StackInfoRenderer(),
19+
structlog.processors.format_exc_info,
20+
structlog.processors.UnicodeDecoder(),
21+
structlog.processors.JSONRenderer(),
22+
],
23+
context_class=dict,
24+
logger_factory=structlog.stdlib.LoggerFactory(),
25+
wrapper_class=structlog.stdlib.BoundLogger,
26+
cache_logger_on_first_use=True,
27+
)

uv.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)