Skip to content

Commit 688dc70

Browse files
authored
feat: allow configs to take in user provided logger instances for client logging behavior (#65)
* feat: add logger field to client configs for user defined sdk logging behavior * fix: update logging configs to ignore debug flag when given given a user provided logger
1 parent c12284f commit 688dc70

File tree

6 files changed

+183
-12
lines changed

6 files changed

+183
-12
lines changed

src/amplitude_experiment/local/client.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import logging
21
from threading import Lock
32
from typing import Any, List, Dict, Set
43

@@ -47,10 +46,7 @@ def __init__(self, api_key: str, config: LocalEvaluationConfig = None):
4746
instance = Amplitude(config.assignment_config.api_key, config.assignment_config)
4847
self.assignment_service = AssignmentService(instance, AssignmentFilter(
4948
config.assignment_config.cache_capacity), config.assignment_config.send_evaluated_props)
50-
self.logger = logging.getLogger("Amplitude")
51-
self.logger.addHandler(logging.StreamHandler())
52-
if self.config.debug:
53-
self.logger.setLevel(logging.DEBUG)
49+
self.logger = self.config.logger
5450
self.__setup_connection_pool()
5551
self.lock = Lock()
5652
self.cohort_storage = InMemoryCohortStorage()

src/amplitude_experiment/local/config.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import logging
2+
import sys
3+
14
from ..assignment import AssignmentConfig
25
from ..cohort.cohort_sync_config import CohortSyncConfig, DEFAULT_COHORT_SYNC_URL, EU_COHORT_SYNC_URL
36
from ..server_zone import ServerZone
@@ -21,7 +24,8 @@ def __init__(self, debug: bool = False,
2124
stream_server_url: str = DEFAULT_STREAM_URL,
2225
stream_flag_conn_timeout: int = 1500,
2326
assignment_config: AssignmentConfig = None,
24-
cohort_sync_config: CohortSyncConfig = None):
27+
cohort_sync_config: CohortSyncConfig = None,
28+
logger: logging.Logger = None):
2529
"""
2630
Initialize a config
2731
Parameters:
@@ -34,6 +38,8 @@ def __init__(self, debug: bool = False,
3438
fetching flag configurations.
3539
assignment_config (AssignmentConfig): The assignment configuration.
3640
cohort_sync_config (CohortSyncConfig): The cohort sync configuration.
41+
logger (logging.Logger): Optional logger instance. If provided, this logger will be used instead of
42+
creating a new one. The debug flag only applies when no logger is provided.
3743
3844
Returns:
3945
The config object
@@ -57,3 +63,17 @@ def __init__(self, debug: bool = False,
5763
self.stream_updates = stream_updates
5864
self.stream_flag_conn_timeout = stream_flag_conn_timeout
5965
self.assignment_config = assignment_config
66+
67+
# Set up logger: use provided logger or create default one
68+
if logger is None:
69+
self.logger = logging.getLogger("Amplitude")
70+
# Only add handler if logger doesn't already have one
71+
if not self.logger.handlers:
72+
handler = logging.StreamHandler(sys.stderr)
73+
self.logger.addHandler(handler)
74+
# Set log level: DEBUG if debug=True, otherwise WARNING
75+
# Only apply debug flag to default logger, not user-provided loggers
76+
log_level = logging.DEBUG if self.debug else logging.WARNING
77+
self.logger.setLevel(log_level)
78+
else:
79+
self.logger = logger

src/amplitude_experiment/remote/client.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import json
2-
import logging
32
import threading
43
import time
54
from time import sleep
@@ -32,10 +31,7 @@ def __init__(self, api_key, config=None):
3231
raise ValueError("Experiment API key is empty")
3332
self.api_key = api_key
3433
self.config = config or RemoteEvaluationConfig()
35-
self.logger = logging.getLogger("Amplitude")
36-
self.logger.addHandler(logging.StreamHandler())
37-
if self.config.debug:
38-
self.logger.setLevel(logging.DEBUG)
34+
self.logger = self.config.logger
3935
self.__setup_connection_pool()
4036

4137
def fetch_v2(self, user: User):

src/amplitude_experiment/remote/config.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
DEFAULT_SERVER_URL = 'https://api.lab.amplitude.com'
44
EU_SERVER_URL = 'https://api.lab.eu.amplitude.com'
5+
import logging
6+
import sys
7+
58

69
class RemoteEvaluationConfig:
710
"""Experiment Remote Client Configuration"""
@@ -14,7 +17,8 @@ def __init__(self, debug=False,
1417
fetch_retry_backoff_max_millis=10000,
1518
fetch_retry_backoff_scalar=1.5,
1619
fetch_retry_timeout_millis=10000,
17-
server_zone: ServerZone = ServerZone.US):
20+
server_zone: ServerZone = ServerZone.US,
21+
logger=None):
1822
"""
1923
Initialize a config
2024
Parameters:
@@ -30,6 +34,8 @@ def __init__(self, debug=False,
3034
fetch_retry_backoff_scalar (float): Scales the minimum backoff exponentially.
3135
fetch_retry_timeout_millis (int): The request timeout for retrying fetch requests.
3236
server_zone (str): Select the Amplitude data center to get flags and variants from, US or EU.
37+
logger (logging.Logger): Optional logger instance. If provided, this logger will be used instead of
38+
creating a new one. The debug flag only applies when no logger is provided.
3339
3440
Returns:
3541
The config object
@@ -46,3 +52,16 @@ def __init__(self, debug=False,
4652
if server_url == DEFAULT_SERVER_URL and server_zone == ServerZone.EU:
4753
self.server_url = EU_SERVER_URL
4854

55+
# Set up logger: use provided logger or create default one
56+
if logger is None:
57+
self.logger = logging.getLogger("Amplitude")
58+
# Only add handler if logger doesn't already have one
59+
if not self.logger.handlers:
60+
handler = logging.StreamHandler(sys.stderr)
61+
self.logger.addHandler(handler)
62+
# Set log level: DEBUG if debug=True, otherwise WARNING
63+
# Only apply debug flag to default logger, not user-provided loggers
64+
log_level = logging.DEBUG if self.debug else logging.WARNING
65+
self.logger.setLevel(log_level)
66+
else:
67+
self.logger = logger

tests/local/config_test.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import io
2+
import logging
3+
import sys
4+
import unittest
5+
6+
from src.amplitude_experiment.local.config import LocalEvaluationConfig
7+
8+
9+
class LocalEvaluationConfigLoggerTestCase(unittest.TestCase):
10+
"""Tests for LocalEvaluationConfig logger configuration"""
11+
12+
def setUp(self):
13+
"""Clear existing handlers from the Amplitude logger before each test"""
14+
logger = logging.getLogger("Amplitude")
15+
logger.handlers.clear()
16+
logger.setLevel(logging.NOTSET)
17+
18+
def tearDown(self):
19+
"""Clean up handlers after each test"""
20+
logger = logging.getLogger("Amplitude")
21+
logger.handlers.clear()
22+
23+
def test_default_logger_has_warning_level(self):
24+
"""Test that default logger has WARNING level and stderr handler when debug=False"""
25+
config = LocalEvaluationConfig(debug=False)
26+
self.assertEqual(config.logger.level, logging.WARNING)
27+
self.assertEqual(len(config.logger.handlers), 1)
28+
handler = config.logger.handlers[0]
29+
self.assertIsInstance(handler, logging.StreamHandler)
30+
self.assertEqual(handler.stream, sys.stderr)
31+
32+
def test_default_logger_has_debug_level_when_debug_true(self):
33+
"""Test that default logger has DEBUG level when debug=True"""
34+
config = LocalEvaluationConfig(debug=True)
35+
self.assertEqual(config.logger.level, logging.DEBUG)
36+
37+
def test_custom_logger_is_used(self):
38+
"""Test that provided custom logger is used"""
39+
custom_logger = logging.getLogger("CustomLogger")
40+
config = LocalEvaluationConfig(logger=custom_logger)
41+
self.assertEqual(config.logger, custom_logger)
42+
self.assertEqual(config.logger.name, "CustomLogger")
43+
44+
def test_custom_logger_level_not_modified_by_debug_flag(self):
45+
"""Test that custom logger level is not modified by debug flag"""
46+
# Test with debug=True
47+
custom_logger = logging.getLogger("CustomLoggerDebug")
48+
custom_logger.setLevel(logging.ERROR)
49+
config = LocalEvaluationConfig(debug=True, logger=custom_logger)
50+
# Logger level should remain unchanged (ERROR), not modified to DEBUG
51+
self.assertEqual(config.logger.level, logging.ERROR)
52+
53+
# Test with debug=False
54+
custom_logger2 = logging.getLogger("CustomLoggerWarning")
55+
custom_logger2.setLevel(logging.INFO)
56+
config2 = LocalEvaluationConfig(debug=False, logger=custom_logger2)
57+
# Logger level should remain unchanged (INFO), not modified to WARNING
58+
self.assertEqual(config2.logger.level, logging.INFO)
59+
60+
def test_default_logger_only_one_handler_added(self):
61+
"""Test that only one handler is added to default logger"""
62+
config1 = LocalEvaluationConfig()
63+
config2 = LocalEvaluationConfig()
64+
# Both should use the same logger instance (singleton)
65+
logger = logging.getLogger("Amplitude")
66+
# Should only have one handler even after creating multiple configs
67+
self.assertEqual(len(logger.handlers), 1)
68+
69+
if __name__ == '__main__':
70+
unittest.main()

tests/remote/config_test.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import io
2+
import logging
3+
import sys
4+
import unittest
5+
6+
from src.amplitude_experiment.remote.config import RemoteEvaluationConfig
7+
8+
9+
class RemoteEvaluationConfigLoggerTestCase(unittest.TestCase):
10+
"""Tests for RemoteEvaluationConfig logger configuration"""
11+
12+
def setUp(self):
13+
"""Clear existing handlers from the Amplitude logger before each test"""
14+
logger = logging.getLogger("Amplitude")
15+
logger.handlers.clear()
16+
logger.setLevel(logging.NOTSET)
17+
18+
def tearDown(self):
19+
"""Clean up handlers after each test"""
20+
logger = logging.getLogger("Amplitude")
21+
logger.handlers.clear()
22+
23+
def test_default_logger_has_warning_level(self):
24+
"""Test that default logger has WARNING level and stderr handler when debug=False"""
25+
config = RemoteEvaluationConfig(debug=False)
26+
self.assertEqual(config.logger.level, logging.WARNING)
27+
self.assertEqual(len(config.logger.handlers), 1)
28+
handler = config.logger.handlers[0]
29+
self.assertIsInstance(handler, logging.StreamHandler)
30+
self.assertEqual(handler.stream, sys.stderr)
31+
32+
def test_default_logger_has_debug_level_when_debug_true(self):
33+
"""Test that default logger has DEBUG level when debug=True"""
34+
config = RemoteEvaluationConfig(debug=True)
35+
self.assertEqual(config.logger.level, logging.DEBUG)
36+
37+
def test_custom_logger_is_used(self):
38+
"""Test that provided custom logger is used"""
39+
custom_logger = logging.getLogger("CustomLogger")
40+
config = RemoteEvaluationConfig(logger=custom_logger)
41+
self.assertEqual(config.logger, custom_logger)
42+
self.assertEqual(config.logger.name, "CustomLogger")
43+
44+
def test_custom_logger_level_not_modified_by_debug_flag(self):
45+
"""Test that custom logger level is not modified by debug flag"""
46+
# Test with debug=True
47+
custom_logger = logging.getLogger("CustomLoggerDebug")
48+
custom_logger.setLevel(logging.ERROR)
49+
config = RemoteEvaluationConfig(debug=True, logger=custom_logger)
50+
# Logger level should remain unchanged (ERROR), not modified to DEBUG
51+
self.assertEqual(config.logger.level, logging.ERROR)
52+
53+
# Test with debug=False
54+
custom_logger2 = logging.getLogger("CustomLoggerWarning")
55+
custom_logger2.setLevel(logging.INFO)
56+
config2 = RemoteEvaluationConfig(debug=False, logger=custom_logger2)
57+
# Logger level should remain unchanged (INFO), not modified to WARNING
58+
self.assertEqual(config2.logger.level, logging.INFO)
59+
60+
def test_default_logger_only_one_handler_added(self):
61+
"""Test that only one handler is added to default logger"""
62+
config1 = RemoteEvaluationConfig()
63+
config2 = RemoteEvaluationConfig()
64+
# Both should use the same logger instance (singleton)
65+
logger = logging.getLogger("Amplitude")
66+
# Should only have one handler even after creating multiple configs
67+
self.assertEqual(len(logger.handlers), 1)
68+
69+
if __name__ == '__main__':
70+
unittest.main()

0 commit comments

Comments
 (0)