Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions src/amplitude_experiment/local/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import logging
from threading import Lock
from typing import Any, List, Dict, Set

Expand Down Expand Up @@ -47,10 +46,7 @@ def __init__(self, api_key: str, config: LocalEvaluationConfig = None):
instance = Amplitude(config.assignment_config.api_key, config.assignment_config)
self.assignment_service = AssignmentService(instance, AssignmentFilter(
config.assignment_config.cache_capacity), config.assignment_config.send_evaluated_props)
self.logger = logging.getLogger("Amplitude")
self.logger.addHandler(logging.StreamHandler())
if self.config.debug:
self.logger.setLevel(logging.DEBUG)
self.logger = self.config.logger
self.__setup_connection_pool()
self.lock = Lock()
self.cohort_storage = InMemoryCohortStorage()
Expand Down
22 changes: 21 additions & 1 deletion src/amplitude_experiment/local/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import logging
import sys

from ..assignment import AssignmentConfig
from ..cohort.cohort_sync_config import CohortSyncConfig, DEFAULT_COHORT_SYNC_URL, EU_COHORT_SYNC_URL
from ..server_zone import ServerZone
Expand All @@ -21,7 +24,8 @@ def __init__(self, debug: bool = False,
stream_server_url: str = DEFAULT_STREAM_URL,
stream_flag_conn_timeout: int = 1500,
assignment_config: AssignmentConfig = None,
cohort_sync_config: CohortSyncConfig = None):
cohort_sync_config: CohortSyncConfig = None,
logger: logging.Logger = None):
"""
Initialize a config
Parameters:
Expand All @@ -34,6 +38,8 @@ def __init__(self, debug: bool = False,
fetching flag configurations.
assignment_config (AssignmentConfig): The assignment configuration.
cohort_sync_config (CohortSyncConfig): The cohort sync configuration.
logger (logging.Logger): Optional logger instance. If provided, this logger will be used instead of
creating a new one. The debug flag only applies when no logger is provided.

Returns:
The config object
Expand All @@ -57,3 +63,17 @@ def __init__(self, debug: bool = False,
self.stream_updates = stream_updates
self.stream_flag_conn_timeout = stream_flag_conn_timeout
self.assignment_config = assignment_config

# Set up logger: use provided logger or create default one
if logger is None:
self.logger = logging.getLogger("Amplitude")
# Only add handler if logger doesn't already have one
if not self.logger.handlers:
handler = logging.StreamHandler(sys.stderr)
self.logger.addHandler(handler)
# Set log level: DEBUG if debug=True, otherwise WARNING
# Only apply debug flag to default logger, not user-provided loggers
log_level = logging.DEBUG if self.debug else logging.WARNING
self.logger.setLevel(log_level)
else:
self.logger = logger
6 changes: 1 addition & 5 deletions src/amplitude_experiment/remote/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json
import logging
import threading
import time
from time import sleep
Expand Down Expand Up @@ -32,10 +31,7 @@ def __init__(self, api_key, config=None):
raise ValueError("Experiment API key is empty")
self.api_key = api_key
self.config = config or RemoteEvaluationConfig()
self.logger = logging.getLogger("Amplitude")
self.logger.addHandler(logging.StreamHandler())
if self.config.debug:
self.logger.setLevel(logging.DEBUG)
self.logger = self.config.logger
self.__setup_connection_pool()

def fetch_v2(self, user: User):
Expand Down
21 changes: 20 additions & 1 deletion src/amplitude_experiment/remote/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

DEFAULT_SERVER_URL = 'https://api.lab.amplitude.com'
EU_SERVER_URL = 'https://api.lab.eu.amplitude.com'
import logging
import sys


class RemoteEvaluationConfig:
"""Experiment Remote Client Configuration"""
Expand All @@ -14,7 +17,8 @@ def __init__(self, debug=False,
fetch_retry_backoff_max_millis=10000,
fetch_retry_backoff_scalar=1.5,
fetch_retry_timeout_millis=10000,
server_zone: ServerZone = ServerZone.US):
server_zone: ServerZone = ServerZone.US,
logger=None):
"""
Initialize a config
Parameters:
Expand All @@ -30,6 +34,8 @@ def __init__(self, debug=False,
fetch_retry_backoff_scalar (float): Scales the minimum backoff exponentially.
fetch_retry_timeout_millis (int): The request timeout for retrying fetch requests.
server_zone (str): Select the Amplitude data center to get flags and variants from, US or EU.
logger (logging.Logger): Optional logger instance. If provided, this logger will be used instead of
creating a new one. The debug flag only applies when no logger is provided.

Returns:
The config object
Expand All @@ -46,3 +52,16 @@ def __init__(self, debug=False,
if server_url == DEFAULT_SERVER_URL and server_zone == ServerZone.EU:
self.server_url = EU_SERVER_URL

# Set up logger: use provided logger or create default one
if logger is None:
self.logger = logging.getLogger("Amplitude")
# Only add handler if logger doesn't already have one
if not self.logger.handlers:
handler = logging.StreamHandler(sys.stderr)
self.logger.addHandler(handler)
# Set log level: DEBUG if debug=True, otherwise WARNING
# Only apply debug flag to default logger, not user-provided loggers
log_level = logging.DEBUG if self.debug else logging.WARNING
self.logger.setLevel(log_level)
else:
self.logger = logger
70 changes: 70 additions & 0 deletions tests/local/config_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import io
import logging
import sys
import unittest

from src.amplitude_experiment.local.config import LocalEvaluationConfig


class LocalEvaluationConfigLoggerTestCase(unittest.TestCase):
"""Tests for LocalEvaluationConfig logger configuration"""

def setUp(self):
"""Clear existing handlers from the Amplitude logger before each test"""
logger = logging.getLogger("Amplitude")
logger.handlers.clear()
logger.setLevel(logging.NOTSET)

def tearDown(self):
"""Clean up handlers after each test"""
logger = logging.getLogger("Amplitude")
logger.handlers.clear()

def test_default_logger_has_warning_level(self):
"""Test that default logger has WARNING level and stderr handler when debug=False"""
config = LocalEvaluationConfig(debug=False)
self.assertEqual(config.logger.level, logging.WARNING)
self.assertEqual(len(config.logger.handlers), 1)
handler = config.logger.handlers[0]
self.assertIsInstance(handler, logging.StreamHandler)
self.assertEqual(handler.stream, sys.stderr)

def test_default_logger_has_debug_level_when_debug_true(self):
"""Test that default logger has DEBUG level when debug=True"""
config = LocalEvaluationConfig(debug=True)
self.assertEqual(config.logger.level, logging.DEBUG)

def test_custom_logger_is_used(self):
"""Test that provided custom logger is used"""
custom_logger = logging.getLogger("CustomLogger")
config = LocalEvaluationConfig(logger=custom_logger)
self.assertEqual(config.logger, custom_logger)
self.assertEqual(config.logger.name, "CustomLogger")

def test_custom_logger_level_not_modified_by_debug_flag(self):
"""Test that custom logger level is not modified by debug flag"""
# Test with debug=True
custom_logger = logging.getLogger("CustomLoggerDebug")
custom_logger.setLevel(logging.ERROR)
config = LocalEvaluationConfig(debug=True, logger=custom_logger)
# Logger level should remain unchanged (ERROR), not modified to DEBUG
self.assertEqual(config.logger.level, logging.ERROR)

# Test with debug=False
custom_logger2 = logging.getLogger("CustomLoggerWarning")
custom_logger2.setLevel(logging.INFO)
config2 = LocalEvaluationConfig(debug=False, logger=custom_logger2)
# Logger level should remain unchanged (INFO), not modified to WARNING
self.assertEqual(config2.logger.level, logging.INFO)

def test_default_logger_only_one_handler_added(self):
"""Test that only one handler is added to default logger"""
config1 = LocalEvaluationConfig()
config2 = LocalEvaluationConfig()
# Both should use the same logger instance (singleton)
logger = logging.getLogger("Amplitude")
# Should only have one handler even after creating multiple configs
self.assertEqual(len(logger.handlers), 1)

if __name__ == '__main__':
unittest.main()
70 changes: 70 additions & 0 deletions tests/remote/config_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import io
import logging
import sys
import unittest

from src.amplitude_experiment.remote.config import RemoteEvaluationConfig


class RemoteEvaluationConfigLoggerTestCase(unittest.TestCase):
"""Tests for RemoteEvaluationConfig logger configuration"""

def setUp(self):
"""Clear existing handlers from the Amplitude logger before each test"""
logger = logging.getLogger("Amplitude")
logger.handlers.clear()
logger.setLevel(logging.NOTSET)

def tearDown(self):
"""Clean up handlers after each test"""
logger = logging.getLogger("Amplitude")
logger.handlers.clear()

def test_default_logger_has_warning_level(self):
"""Test that default logger has WARNING level and stderr handler when debug=False"""
config = RemoteEvaluationConfig(debug=False)
self.assertEqual(config.logger.level, logging.WARNING)
self.assertEqual(len(config.logger.handlers), 1)
handler = config.logger.handlers[0]
self.assertIsInstance(handler, logging.StreamHandler)
self.assertEqual(handler.stream, sys.stderr)

def test_default_logger_has_debug_level_when_debug_true(self):
"""Test that default logger has DEBUG level when debug=True"""
config = RemoteEvaluationConfig(debug=True)
self.assertEqual(config.logger.level, logging.DEBUG)

def test_custom_logger_is_used(self):
"""Test that provided custom logger is used"""
custom_logger = logging.getLogger("CustomLogger")
config = RemoteEvaluationConfig(logger=custom_logger)
self.assertEqual(config.logger, custom_logger)
self.assertEqual(config.logger.name, "CustomLogger")

def test_custom_logger_level_not_modified_by_debug_flag(self):
"""Test that custom logger level is not modified by debug flag"""
# Test with debug=True
custom_logger = logging.getLogger("CustomLoggerDebug")
custom_logger.setLevel(logging.ERROR)
config = RemoteEvaluationConfig(debug=True, logger=custom_logger)
# Logger level should remain unchanged (ERROR), not modified to DEBUG
self.assertEqual(config.logger.level, logging.ERROR)

# Test with debug=False
custom_logger2 = logging.getLogger("CustomLoggerWarning")
custom_logger2.setLevel(logging.INFO)
config2 = RemoteEvaluationConfig(debug=False, logger=custom_logger2)
# Logger level should remain unchanged (INFO), not modified to WARNING
self.assertEqual(config2.logger.level, logging.INFO)

def test_default_logger_only_one_handler_added(self):
"""Test that only one handler is added to default logger"""
config1 = RemoteEvaluationConfig()
config2 = RemoteEvaluationConfig()
# Both should use the same logger instance (singleton)
logger = logging.getLogger("Amplitude")
# Should only have one handler even after creating multiple configs
self.assertEqual(len(logger.handlers), 1)

if __name__ == '__main__':
unittest.main()