Skip to content

Commit 69ff84c

Browse files
committed
WIP
TODO: Message inspection to split bulk messages TODO: Log level guessing outside of filter
1 parent a71ae41 commit 69ff84c

File tree

4 files changed

+56
-16
lines changed

4 files changed

+56
-16
lines changed

src/apify_client/_logging.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from contextvars import ContextVar
88
from typing import TYPE_CHECKING, Any, Callable, NamedTuple, cast
99

10+
from colorama import Style, Fore
11+
1012
# Conditional import only executed when type checking, otherwise we'd get circular dependency issues
1113
if TYPE_CHECKING:
1214
from apify_client.clients.base.base_client import _BaseBaseClient
@@ -121,7 +123,6 @@ def format(self, record: logging.LogRecord) -> str:
121123
log_string = f'{log_string} ({json.dumps(extra)})'
122124
return log_string
123125

124-
125126
def create_redirect_logger(
126127
name: str,
127128
*,
@@ -141,7 +142,7 @@ def create_redirect_logger(
141142
The created logger.
142143
"""
143144
to_logger = logging.getLogger(name)
144-
to_logger.propagate = True
145+
145146
if respect_original_log_level:
146147
to_logger.addFilter(
147148
_RedirectLogLevelFilter(
@@ -151,6 +152,11 @@ def create_redirect_logger(
151152
else:
152153
to_logger.addFilter(_FixedLevelFilter(default_level=default_level))
153154

155+
to_logger.propagate = False
156+
handler = logging.StreamHandler()
157+
handler.setFormatter(RedirectLogFormatter())
158+
to_logger.addHandler(handler)
159+
to_logger.setLevel(logging.INFO)
154160
return to_logger
155161

156162

@@ -187,3 +193,17 @@ def filter(self, record: logging.LogRecord) -> bool:
187193
record.levelno = self._guess_log_level_from_message(record.msg)
188194
record.levelname = logging.getLevelName(record.levelno)
189195
return True
196+
197+
class RedirectLogFormatter:
198+
"""Formater applied to default redirect logger."""
199+
200+
def format(self, record: logging.LogRecord) -> str:
201+
"""Format the log by prepending logger name to the original message.
202+
203+
TODO: Make advanced coloring later.
204+
Ideally it should respect the color of the original log, but that information is not available in the API.
205+
Inspecting logs and coloring their parts during runtime could be quite heavy. Keep it simple for now.
206+
"""
207+
logger_name_string = f'{Fore.CYAN}[{record.name}]{Style.RESET_ALL} '
208+
209+
return f'{logger_name_string}-> {record.msg}'

src/apify_client/clients/resource_clients/log.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ async def _stream_log(self, to_logger: logging.Logger) -> None:
224224
async for data in log_stream.aiter_bytes():
225225
log_level = logging.INFO # The Original log level is not known unless the message is inspected.
226226
# Adjust the log level in custom logger filter if needed.
227-
to_logger.log(level=log_level, msg=data)
227+
228+
# Split by lines for each line that does start with standard format, try to guess the log level
229+
# example split marker: \n2025-05-12T15:35:59.429Z
230+
231+
to_logger.log(level=log_level, msg=data.decode('utf-8'))
232+
#logging.getLogger("apify_client").info(data)
228233
# Cleanup in the end
229234
#log_stream.close()

src/apify_client/clients/resource_clients/run.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,8 @@ async def get_streamed_log(self, to_logger: logging.Logger | None = None, actor_
533533
run_id = run_data.get('id', '') if run_data else ''
534534

535535
if not to_logger:
536-
to_logger = create_redirect_logger(f'apify.{f"{actor_name}-{run_id}"}')
536+
name = "-".join(part for part in (actor_name, run_id) if part)
537+
to_logger = create_redirect_logger(f'apify.{name}')
537538

538539
return StreamedLogAsync(self.log(), to_logger)
539540

tests/unit/test_logging.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import asyncio
2+
import json
3+
import logging
14
import time
25
from collections.abc import AsyncIterator
36

@@ -9,31 +12,42 @@
912
from apify_client.clients import RunClientAsync
1013

1114

12-
13-
1415
@respx.mock
1516
async def test_redirected_logs(caplog) -> None:
1617
"""Test that redirected logs are formatted correctly."""
18+
mocked_actor_logs_logs = (b"INFO a", b"WARNING b", b"DEBUG c")
19+
mocked_actor_name = "mocked_actor"
1720

1821
class AsyncByteStream:
1922
async def __aiter__(self) -> AsyncIterator[bytes]:
20-
for i in range(2):
21-
yield b"Some text"
22-
time.sleep(1)
23-
print("b")
23+
for i in mocked_actor_logs_logs:
24+
yield i
25+
await asyncio.sleep(0.1)
2426

2527
async def aclose(self) -> None:
26-
print("a")
2728
pass
2829

2930
run_client = ApifyClientAsync(token="mocked_token", api_url='https://example.com').run(run_id="run_is_mocked")
3031
respx.get(url='https://example.com/v2/actor-runs/run_is_mocked').mock(
32+
return_value=httpx.Response(content=json.dumps({"data":{'actId': 'SbjD4JEucMevUdQAH'}}),status_code=200))
33+
respx.get(url='https://example.com/v2/actor-runs/run_is_mocked/log?stream=1').mock(
3134
return_value=httpx.Response(stream=AsyncByteStream(), status_code=200))
3235
# {'http_version': b'HTTP/1.1', 'network_stream': <httpcore._backends.anyio.AnyIOStream object at 0x7fc82543db70>, 'reason_phrase': b'OK'}
3336
# [(b'Date', b'Mon, 12 May 2025 13:24:41 GMT'), (b'Content-Type', b'application/json; charset=utf-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Cache-Control', b'no-cache, no-store, must-revalidate'), (b'Pragma', b'no-cache'), (b'Expires', b'0'), (b'Access-Control-Allow-Origin', b'*'), (b'Access-Control-Allow-Headers', b'User-Agent, Content-Type, Authorization, X-Apify-Request-Origin, openai-conversation-id, openai-ephemeral-user-id'), (b'Access-Control-Allow-Methods', b'GET, POST'), (b'Access-Control-Expose-Headers', b'X-Apify-Pagination-Total, X-Apify-Pagination-Offset, X-Apify-Pagination-Desc, X-Apify-Pagination-Count, X-Apify-Pagination-Limit'), (b'Referrer-Policy', b'no-referrer'), (b'X-Robots-Tag', b'none'), (b'X-RateLimit-Limit', b'200'), (b'Location', b'https://api.apify.com/v2/actor-runs/ywNUnFFbOksQLa4mH'), (b'Vary', b'Accept-Encoding'), (b'Content-Encoding', b'gzip')]
34-
streamed_log = await run_client.get_streamed_log(actor_name="mocked_actor")
35-
async with streamed_log:
36-
# do some stuff
37-
pass
37+
streamed_log = await run_client.get_streamed_log(actor_name=mocked_actor_name)
38+
39+
with caplog.at_level(logging.DEBUG):
40+
async with streamed_log:
41+
await asyncio.sleep(1)
42+
# do some stuff
43+
pass
44+
45+
records = caplog.records
46+
assert len(records) == 2
47+
48+
49+
50+
"""
3851
39-
records = caplog.get_records()
52+
{'actId': 'SbjD4JEucMevUdQAH', 'buildId': 'Jv7iIjo1JV0gEXQEm', 'buildNumber': '0.0.5', 'containerUrl': 'https://tlo2axp6qbc7.runs.apify.net', 'defaultDatasetId': 'DZq6uDwZ4gSXev8h2', 'defaultKeyValueStoreId': '7UswAGyvNKFGlddHS', 'defaultRequestQueueId': 'Gk4ye89GRCoqFNdsM', 'finishedAt': None, 'generalAccess': 'FOLLOW_USER_SETTING', 'id': 'u6Q52apBHWO09NjDP', 'meta': {'origin': 'API', 'userAgent': 'ApifyClient/1.9.0 (linux; Python/3.10.12); isAtHome/False'}, 'options': {'build': 'latest', 'diskMbytes': 2048, 'memoryMbytes': 1024, 'timeoutSecs': 3600}, 'startedAt': '2025-05-12T13:54:23.028Z', 'stats': {'computeUnits': 0, 'inputBodyLen': 15, 'migrationCount': 0, 'rebootCount': 0, 'restartCount': 0, 'resurrectCount': 0}, 'status': 'READY', 'usage': {'ACTOR_COMPUTE_UNITS': 0, 'DATASET_READS': 0, 'DATASET_WRITES': 0, 'DATA_TRANSFER_EXTERNAL_GBYTES': 0, 'DATA_TRANSFER_INTERNAL_GBYTES': 0, 'KEY_VALUE_STORE_LISTS': 0, 'KEY_VALUE_STORE_READS': 0, 'KEY_VALUE_STORE_WRITES': 1, 'PROXY_RESIDENTIAL_TRANSFER_GBYTES': 0, 'PROXY_SERPS': 0, 'REQUEST_QUEUE_READS': 0, 'REQUEST_QUEUE_WRITES': 0}, 'usageTotalUsd': 5e-05, 'usageUsd': {'ACTOR_COMPUTE_UNITS': 0, 'DATASET_READS': 0, 'DATASET_WRITES': 0, 'DATA_TRANSFER_EXTERNAL_GBYTES': 0, 'DATA_TRANSFER_INTERNAL_GBYTES': 0, 'KEY_VALUE_STORE_LISTS': 0, 'KEY_VALUE_STORE_READS': 0, 'KEY_VALUE_STORE_WRITES': 5e-05, 'PROXY_RESIDENTIAL_TRANSFER_GBYTES': 0, 'PROXY_SERPS': 0, 'REQUEST_QUEUE_READS': 0, 'REQUEST_QUEUE_WRITES': 0}, 'userId': 'LjAzEG1CadliECnrn'}
53+
"""

0 commit comments

Comments
 (0)