Skip to content

Commit cdd5145

Browse files
authored
feat: implement logger (#80)
* feat: implement logger Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add logger to the client Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add logging in executable Signed-off-by: dosi <dosi.kolev@limechain.tech> * test: add logger in unit/integration tests Signed-off-by: dosi <dosi.kolev@limechain.tech> * refactor: log more information when switching nodes, change the default log level to be ERROR Signed-off-by: dosi <dosi.kolev@limechain.tech> * test: add unit tests for logger Signed-off-by: dosi <dosi.kolev@limechain.tech> * refactor: log requestId when maximum attempts are exceeded in executable Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add example for logging workflow Signed-off-by: dosi <dosi.kolev@limechain.tech> * chore: remove empty lines Signed-off-by: dosi <dosi.kolev@limechain.tech> * refactor: use set_silent instead of set_level in logging_example.py Signed-off-by: dosi <dosi.kolev@limechain.tech> --------- Signed-off-by: dosi <dosi.kolev@limechain.tech>
1 parent 94f8bb5 commit cdd5145

File tree

8 files changed

+407
-6
lines changed

8 files changed

+407
-6
lines changed

examples/logging_example.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import os
2+
import sys
3+
from dotenv import load_dotenv
4+
5+
from hiero_sdk_python import (
6+
Client,
7+
AccountId,
8+
PrivateKey,
9+
AccountCreateTransaction,
10+
Network,
11+
)
12+
from hiero_sdk_python.logger.logger import Logger
13+
from hiero_sdk_python.logger.log_level import LogLevel
14+
15+
load_dotenv()
16+
17+
def show_logging_workflow():
18+
"""Function to demonstrate logging functionality in the Hiero SDK."""
19+
20+
# Retrieving network type from environment variable HEDERA_NETWORK
21+
network_name = os.getenv('HEDERA_NETWORK', 'testnet')
22+
23+
# Network setup
24+
network = Network(network_name)
25+
client = Client(network)
26+
27+
# The client comes with default logger.
28+
# We can create logger and replace the default logger of this client.
29+
# Create a custom logger with DEBUG level
30+
logger = Logger(level=LogLevel.DEBUG, name="hiero_sdk_python")
31+
# Replace the default logger
32+
client.logger = logger
33+
34+
# Set the logging level for this client's logger
35+
client.logger.set_level(LogLevel.TRACE)
36+
37+
# Retrieving operator ID from environment variable OPERATOR_ID
38+
operator_id = AccountId.from_string(os.getenv('OPERATOR_ID'))
39+
40+
# Retrieving operator key from environment variable OPERATOR_KEY
41+
operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY'))
42+
43+
# Setting the client operator ID and key
44+
client.set_operator(operator_id, operator_key)
45+
46+
# Generate new key to use with new account
47+
new_key = PrivateKey.generate()
48+
49+
print(f"Private key: {new_key.to_string()}")
50+
print(f"Public key: {new_key.public_key().to_string()}")
51+
52+
# Transaction used to show trace level logging functionality from client
53+
transaction = (
54+
AccountCreateTransaction()
55+
.set_key(new_key.public_key())
56+
.set_initial_balance(100000000) # 1 HBAR in tinybars
57+
.freeze_with(client)
58+
.sign(operator_key)
59+
)
60+
61+
try:
62+
receipt = transaction.execute(client)
63+
print(f"Account creation with client trace level logging successful. Account ID: {receipt.accountId}")
64+
except Exception as e:
65+
print(f"Account creation failed: {str(e)}")
66+
67+
# Logger can be disabled
68+
client.logger.set_silent(True)
69+
70+
# Create account transaction used with disabled logger
71+
transaction = (
72+
AccountCreateTransaction()
73+
.set_key(new_key.public_key())
74+
.set_initial_balance(100000000) # 1 HBAR in tinybars
75+
.freeze_with(client)
76+
.sign(operator_key)
77+
)
78+
79+
try:
80+
receipt = transaction.execute(client)
81+
print(f"Account creation with disabled logging successful. Account ID: {receipt.accountId}")
82+
except Exception as e:
83+
print(f"Account creation failed: {str(e)}")
84+
85+
if __name__ == "__main__":
86+
show_logging_workflow()

src/hiero_sdk_python/client/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from hiero_sdk_python.logger.logger import Logger, LogLevel
12
import grpc
23
from collections import namedtuple
34

@@ -38,6 +39,8 @@ def __init__(self, network=None):
3839
self._switch_node(initial_node_id)
3940

4041
self._init_mirror_stub()
42+
43+
self.logger = Logger(LogLevel.from_env(), "hiero_sdk_python")
4144

4245
def _init_mirror_stub(self):
4346
"""

src/hiero_sdk_python/executable.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from os import error
12
import time
23
import typing
34
import grpc
@@ -6,7 +7,6 @@
67

78
from hiero_sdk_python.channels import _Channel
89
from hiero_sdk_python.exceptions import MaxAttemptsError
9-
1010
if typing.TYPE_CHECKING:
1111
from hiero_sdk_python.client.client import Client
1212

@@ -138,6 +138,12 @@ def _map_response(self, response, node_id, proto_request):
138138
The appropriate response object for the operation
139139
"""
140140
raise NotImplementedError("_map_response must be implemented by subclasses")
141+
142+
def _get_request_id(self):
143+
"""
144+
Format the request ID for the logger.
145+
"""
146+
return f"{self.__class__.__name__}:{time.time_ns()}"
141147

142148
def _execute(self, client: "Client"):
143149
"""
@@ -160,17 +166,23 @@ def _execute(self, client: "Client"):
160166
max_attempts = client.max_attempts
161167
current_backoff = self._min_backoff
162168
err_persistant = None
163-
169+
170+
tx_id = self.transaction_id if hasattr(self, "transaction_id") else None
171+
172+
logger = client.logger
173+
164174
for attempt in range(max_attempts):
165175
# Exponential backoff for retries
166176
if attempt > 0 and current_backoff < self._max_backoff:
167177
current_backoff *= 2
168-
178+
169179
# Create a channel wrapper from the client's channel
170180
channel = _Channel(client.channel)
171181

172182
# Set the node account id to the client's node account id
173183
self.node_account_id = client.node_account_id
184+
185+
logger.trace("Executing", "requestId", self._get_request_id(), "nodeAccountID", self.node_account_id, "attempt", attempt + 1, "maxAttempts", max_attempts)
174186

175187
# Get the appropriate gRPC method to call
176188
method = self._get_method(channel)
@@ -179,6 +191,8 @@ def _execute(self, client: "Client"):
179191
proto_request = self._make_request()
180192

181193
try:
194+
logger.trace("Executing gRPC call", "requestId", self._get_request_id())
195+
182196
# Execute the transaction method with the protobuf request
183197
response = _execute_method(method, proto_request)
184198

@@ -188,19 +202,22 @@ def _execute(self, client: "Client"):
188202
# Determine if we should retry based on the response
189203
execution_state = self._should_retry(response)
190204

205+
logger.trace(f"{self.__class__.__name__} status received", "nodeAccountID", self.node_account_id, "network", client.network.network, "state", execution_state.name, "txID", tx_id)
206+
191207
# Handle the execution state
192208
match execution_state:
193209
case _ExecutionState.RETRY:
194210
# If we should retry, wait for the backoff period and try again
195211
err_persistant = status_error
196-
_delay_for_attempt(current_backoff)
212+
_delay_for_attempt(self._get_request_id(), current_backoff, attempt, logger, err_persistant)
197213
continue
198214
case _ExecutionState.EXPIRED:
199215
raise status_error
200216
case _ExecutionState.ERROR:
201217
raise status_error
202218
case _ExecutionState.FINISHED:
203219
# If the transaction completed successfully, map the response and return it
220+
logger.trace(f"{self.__class__.__name__} finished execution")
204221
return self._map_response(response, client.node_account_id, proto_request)
205222
except grpc.RpcError as e:
206223
# Save the error
@@ -210,19 +227,23 @@ def _execute(self, client: "Client"):
210227
node_index = (attempt + 1) % len(node_account_ids)
211228
current_node_account_id = node_account_ids[node_index]
212229
client._switch_node(current_node_account_id)
230+
logger.trace("Switched to a different node for the next attempt", "error", err_persistant, "from node", self.node_account_id, "to node", current_node_account_id)
213231
continue
232+
233+
logger.error("Exceeded maximum attempts for request", "requestId", self._get_request_id(), "last exception being", err_persistant)
214234

215235
raise MaxAttemptsError("Exceeded maximum attempts for request", client.node_account_id, err_persistant)
216236

217237

218-
def _delay_for_attempt(current_backoff: int):
238+
def _delay_for_attempt(request_id: str, current_backoff: int, attempt: int, logger, error):
219239
"""
220240
Delay for the specified backoff period before retrying.
221241
222242
Args:
223243
attempt (int): The current attempt number (0-based)
224244
current_backoff (int): The current backoff period in milliseconds
225245
"""
246+
logger.trace(f"Retrying request attempt", "requestId", request_id, "delay", current_backoff, "attempt", attempt, "error", error)
226247
time.sleep(current_backoff * 0.001)
227248

228249
def _execute_method(method, proto_request):
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
Log level module for the Hiero SDK.
3+
4+
This module defines the log levels used throughout the SDK.
5+
"""
6+
7+
import os
8+
from enum import IntEnum
9+
10+
class LogLevel(IntEnum):
11+
"""
12+
Enumeration of log levels
13+
"""
14+
TRACE = 5
15+
DEBUG = 10
16+
INFO = 20
17+
WARN = 30
18+
ERROR = 40
19+
CRITICAL = 50
20+
DISABLED = 60
21+
22+
def to_python_level(self):
23+
"""Convert to Python's logging level
24+
25+
Returns:
26+
int: The Python logging level
27+
"""
28+
return self.value
29+
30+
@classmethod
31+
def from_string(cls, level_str) -> 'LogLevel':
32+
"""Convert a string to a LogLevel
33+
34+
Args:
35+
level_str: The string to convert
36+
37+
Returns:
38+
LogLevel: The LogLevel enum value
39+
"""
40+
if level_str is None:
41+
return cls.ERROR
42+
43+
try:
44+
return cls[level_str.upper()]
45+
except KeyError:
46+
raise ValueError(f"Invalid log level: {level_str}")
47+
48+
@classmethod
49+
def from_env(cls):
50+
"""
51+
Get log level from environment variable
52+
53+
Returns:
54+
LogLevel: The LogLevel enum value
55+
"""
56+
level_str = os.getenv('LOG_LEVEL')
57+
return cls.from_string(level_str)

0 commit comments

Comments
 (0)