Skip to content

Commit 1906bb2

Browse files
authored
feat: Automatically configure logging (#271)
- blocked by crawlee 0.3.5 release
1 parent 6e7d19b commit 1906bb2

File tree

8 files changed

+66
-24
lines changed

8 files changed

+66
-24
lines changed

docs/04-upgrading/upgrading_to_v20.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Attributes suffixed with `_millis` were renamed to remove said suffix and have t
2222
- The `Actor.main` method has been removed as it brings no benefits compared to using `async with Actor`.
2323
- The `Actor.add_webhook`, `Actor.start`, `Actor.call` and `Actor.start_task` methods now accept instances of the `apify.Webhook` model instead of an untyped `dict`.
2424
- `Actor.start`, `Actor.call`, `Actor.start_task`, `Actor.set_status_message` and `Actor.abort` return instances of the `ActorRun` model instead of an untyped `dict`.
25+
- Upon entering the context manager (`async with Actor`), the `Actor` puts the default logging configuration in place. This can be disabled using the `configure_logging` parameter.
26+
- The `config` parameter of `Actor` has been renamed to `configuration`.
2527

2628
## Scrapy integration
2729

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ keywords = [
4848
python = "^3.9"
4949
apify-client = ">=1.7.1"
5050
apify-shared = ">=1.1.2"
51-
crawlee = ">=0.3.0"
51+
crawlee = ">=0.3.5"
5252
cryptography = ">=42.0.0"
5353
httpx = ">=0.27.0"
5454
lazy-object-proxy = ">=1.10.0"

src/apify/_actor.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from apify._proxy_configuration import ProxyConfiguration
2525
from apify._utils import get_system_info, is_running_in_ipython
2626
from apify.apify_storage_client import ApifyStorageClient
27-
from apify.log import logger
27+
from apify.log import _configure_logging, logger
2828
from apify.storages import Dataset, KeyValueStore, RequestQueue
2929

3030
if TYPE_CHECKING:
@@ -46,16 +46,24 @@ class _ActorType:
4646
_configuration: Configuration
4747
_is_exiting = False
4848

49-
def __init__(self, config: Configuration | None = None) -> None:
49+
def __init__(
50+
self,
51+
configuration: Configuration | None = None,
52+
*,
53+
configure_logging: bool = True,
54+
) -> None:
5055
"""Create an Actor instance.
5156
5257
Note that you don't have to do this, all the functionality is accessible using the default instance
5358
(e.g. `Actor.open_dataset()`).
5459
5560
Args:
56-
config: The Actor configuration to be used. If not passed, a new Configuration instance will be created.
61+
configuration: The Actor configuration to be used. If not passed, a new Configuration instance will
62+
be created.
63+
configure_logging: Should the default logging configuration be configured?
5764
"""
58-
self._configuration = config or Configuration.get_global_configuration()
65+
self._configuration = configuration or Configuration.get_global_configuration()
66+
self._configure_logging = configure_logging
5967
self._apify_client = self.new_client()
6068

6169
self._event_manager: EventManager
@@ -81,6 +89,9 @@ async def __aenter__(self) -> Self:
8189
When you exit the `async with` block, the `Actor.exit()` method is called, and if any exception happens while
8290
executing the block code, the `Actor.fail` method is called.
8391
"""
92+
if self._configure_logging:
93+
_configure_logging(self._configuration)
94+
8495
await self.init()
8596
return self
8697

@@ -111,15 +122,20 @@ def __repr__(self) -> str:
111122

112123
return super().__repr__()
113124

114-
def __call__(self, config: Configuration) -> Self:
125+
def __call__(self, configuration: Configuration | None = None, *, configure_logging: bool = True) -> Self:
115126
"""Make a new Actor instance with a non-default configuration."""
116-
return self.__class__(config=config)
127+
return self.__class__(configuration=configuration, configure_logging=configure_logging)
117128

118129
@property
119130
def apify_client(self) -> ApifyClientAsync:
120131
"""The ApifyClientAsync instance the Actor instance uses."""
121132
return self._apify_client
122133

134+
@property
135+
def configuration(self) -> Configuration:
136+
"""The Configuration instance the Actor instance uses."""
137+
return self._configuration
138+
123139
@property
124140
def config(self) -> Configuration:
125141
"""The Configuration instance the Actor instance uses."""

src/apify/log.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
from __future__ import annotations
22

33
import logging
4+
from typing import TYPE_CHECKING
45

5-
from crawlee._log_config import CrawleeLogFormatter
6+
from crawlee._log_config import CrawleeLogFormatter, configure_logger, get_configured_log_level
7+
8+
if TYPE_CHECKING:
9+
from apify import Configuration
610

711
# Name of the logger used throughout the library (resolves to 'apify')
812
logger_name = __name__.split('.')[0]
@@ -13,3 +17,27 @@
1317

1418
class ActorLogFormatter(CrawleeLogFormatter): # noqa: D101 Inherited from parent class
1519
pass
20+
21+
22+
def _configure_logging(configuration: Configuration) -> None:
23+
apify_client_logger = logging.getLogger('apify_client')
24+
configure_logger(apify_client_logger, configuration, remove_old_handlers=True)
25+
26+
level = get_configured_log_level(configuration)
27+
28+
# Keep apify_client logger quiet unless debug logging is requested
29+
if level > logging.DEBUG:
30+
apify_client_logger.setLevel(logging.INFO)
31+
else:
32+
apify_client_logger.setLevel(level)
33+
34+
# Silence HTTPX logger unless debug logging is requested
35+
httpx_logger = logging.getLogger('httpx')
36+
if level > logging.DEBUG:
37+
httpx_logger.setLevel(logging.WARNING)
38+
else:
39+
httpx_logger.setLevel(level)
40+
41+
# Use configured log level for apify logger
42+
apify_logger = logging.getLogger('apify')
43+
configure_logger(apify_logger, configuration, remove_old_handlers=True)

tests/integration/test_actor_log.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,11 @@ async def test_actor_log(self: TestActorLog, make_actor: ActorFactory) -> None:
1313
async def main() -> None:
1414
import logging
1515

16-
from apify.log import ActorLogFormatter, logger
17-
18-
# Clear any other log handlers, so they don't mess with this test
19-
client_logger = logging.getLogger('apify_client')
20-
apify_logger = logging.getLogger('apify')
21-
client_logger.handlers.clear()
22-
apify_logger.handlers.clear()
23-
24-
# Set handler only on the 'apify' logger
25-
apify_logger.setLevel(logging.DEBUG)
26-
handler = logging.StreamHandler()
27-
handler.setFormatter(ActorLogFormatter())
28-
apify_logger.addHandler(handler)
16+
from apify.log import logger
2917

3018
async with Actor:
19+
logger.setLevel(logging.DEBUG)
20+
3121
# Test Actor.log
3222
Actor.log.debug('Debug message')
3323
Actor.log.info('Info message')
@@ -82,7 +72,7 @@ async def main() -> None:
8272
assert run_log_lines.pop(0) == '[apify] ERROR Error message'
8373
assert run_log_lines.pop(0) == '[apify] ERROR Exception message'
8474
assert run_log_lines.pop(0) == ' Traceback (most recent call last):'
85-
assert run_log_lines.pop(0) == ' File "/usr/src/app/src/main.py", line 35, in main'
75+
assert run_log_lines.pop(0) == ' File "/usr/src/app/src/main.py", line 25, in main'
8676
assert run_log_lines.pop(0) == " raise ValueError('Dummy ValueError')"
8777
assert run_log_lines.pop(0) == ' ValueError: Dummy ValueError'
8878
assert run_log_lines.pop(0) == '[apify] INFO Multi'
@@ -91,7 +81,7 @@ async def main() -> None:
9181
assert run_log_lines.pop(0) == 'message'
9282
assert run_log_lines.pop(0) == '[apify] ERROR Actor failed with an exception'
9383
assert run_log_lines.pop(0) == ' Traceback (most recent call last):'
94-
assert run_log_lines.pop(0) == ' File "/usr/src/app/src/main.py", line 43, in main'
84+
assert run_log_lines.pop(0) == ' File "/usr/src/app/src/main.py", line 33, in main'
9585
assert run_log_lines.pop(0) == " raise RuntimeError('Dummy RuntimeError')"
9686
assert run_log_lines.pop(0) == ' RuntimeError: Dummy RuntimeError'
9787
assert run_log_lines.pop(0) == '[apify] INFO Exiting Actor ({"exit_code": 91})'

tests/unit/actor/test_actor_env_helpers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ async def test_get_env_use_env_vars(self, monkeypatch: pytest.MonkeyPatch) -> No
4949
ApifyEnvVars.DEFAULT_REQUEST_QUEUE_ID,
5050
ApifyEnvVars.SDK_LATEST_VERSION,
5151
ApifyEnvVars.LOG_FORMAT,
52+
ApifyEnvVars.LOG_LEVEL,
5253
}
5354

5455
legacy_env_vars = {
@@ -65,6 +66,8 @@ async def test_get_env_use_env_vars(self, monkeypatch: pytest.MonkeyPatch) -> No
6566

6667
# Set up random env vars
6768
expected_get_env: dict[str, Any] = {}
69+
expected_get_env[ApifyEnvVars.LOG_LEVEL.name.lower()] = 'INFO'
70+
6871
for int_env_var in INTEGER_ENV_VARS:
6972
if int_env_var in ignored_env_vars:
7073
continue

tests/unit/actor/test_actor_helpers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ async def test_actor_metamorpth_not_work_locally(
134134
self: TestActorMethodsWorksOnlyOnPlatform,
135135
caplog: pytest.LogCaptureFixture,
136136
) -> None:
137+
caplog.set_level('WARNING')
137138
async with Actor:
138139
await Actor.metamorph('random-id')
139140

@@ -145,6 +146,7 @@ async def test_actor_reboot_not_work_locally(
145146
self: TestActorMethodsWorksOnlyOnPlatform,
146147
caplog: pytest.LogCaptureFixture,
147148
) -> None:
149+
caplog.set_level('WARNING')
148150
async with Actor:
149151
await Actor.reboot()
150152

@@ -156,6 +158,7 @@ async def test_actor_add_webhook_not_work_locally(
156158
self: TestActorMethodsWorksOnlyOnPlatform,
157159
caplog: pytest.LogCaptureFixture,
158160
) -> None:
161+
caplog.set_level('WARNING')
159162
async with Actor:
160163
await Actor.add_webhook(
161164
Webhook(event_types=[WebhookEventType.ACTOR_BUILD_ABORTED], request_url='https://example.com')

tests/unit/actor/test_actor_log.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async def test_actor_log(
2424
monkeypatch.setenv('APIFY_IS_AT_HOME', '1')
2525

2626
with contextlib.suppress(RuntimeError):
27-
async with Actor:
27+
async with Actor(configure_logging=False):
2828
# Test Actor.log
2929
Actor.log.debug('Debug message')
3030
Actor.log.info('Info message')

0 commit comments

Comments
 (0)