Skip to content

feat: Add redirected actor logs #403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
May 20, 2025
Merged

feat: Add redirected actor logs #403

merged 19 commits into from
May 20, 2025

Conversation

Pijukatel
Copy link
Contributor

@Pijukatel Pijukatel commented May 13, 2025

Description

  • Add convenience methods and arguments for redirecting streamed actor log into another logger.
  • This changes the default behavior in non breaking way. By default the logs will be redirected now.
  • Update log endpoints calls to accept raw parameter.

Example usage:

Starting new actor run through call

3 different use-cases:

...
async with Actor:
    # Default logger
    await Actor.new_client().actor(actor_id="...").call(timeout_secs=5)

    # No logger
    await Actor.new_client().actor(actor_id="...").call(timeout_secs=5, logger = None)

    # Custom logger
    custom_logger = logging.getLogger("apify.kustom_logga")
    handler = logging.StreamHandler()
    custom_logger.addHandler(handler)
    custom_logger.propagate = False

    await Actor.new_client().actor(actor_id="SbjD4JEucMevUdQAH").call(timeout_secs=5, logger = custom_logger)

Attaching to already running actor, redirecting all logs from actor start

(This is the technique to be used when starting actor through actor.start as well)

...
async with Actor:
    ...
    # With context manager
    async with await Actor.new_client().run("some_id").get_streamed_log():
        await asyncio.sleep(5)
        # Logging will stop out of context

     # With direct call (lifecycle of logging has to be manually handled)
    streamed_log = await Actor.new_client().run("some_id").get_streamed_log()
    streamed_log.start()
    await asyncio.sleep(5)
    streamed_log.close()

Attaching to already running actor, redirecting only new logs

(This can be useful when communicating with some long-running standby actor)

...
async with Actor:
    # With context manager
    async with await Actor.new_client().run("some_id").get_streamed_log(from_start=False):
        await asyncio.sleep(5)
        # Logging will stop out of context

Example actor with all the logging options shown:

https://console.apify.com/actors/m7ZFYX42SCRFOL0JW

Issues

Pijukatel added 4 commits May 12, 2025 15:44
TODO: Message inspection to split bulk messages
TODO: Log level guessing outside of filter
Update test to be able to use caplog
@Pijukatel
Copy link
Contributor Author

Pijukatel commented May 13, 2025

This is a minimal draft version with only async client updated so far to have early review on the concept (sync client will be done later).

Such change can be later propagated to Actor class in sdk.

Example usage shown on the screenshot, where we call actor called logger three times with different logging arguments:

1. Default - redirect logger (will be new default behavior), user do not need to do anything and some predefined reasonable logger is used. (Logger name is f"apify.{actor}-{run_id}")
2. No redirect logger (same as current behavior, but have to be selected by explicitly passing None to logger argument)
3. Custom logger defined by the user (I have created some example logger for the sake of the example, but it can be anything.)

image

Redirected messages have estimated logging level, so in most cases you can figure out what level it was in the original log and set it accordingly in this log.

Example actor: https://console.apify.com/actors/m7ZFYX42SCRFOL0JW/source

Not in the scope of first version: Respecting the coloring of the original logs -> This information is not available in the logs returned from the api call, so we could try to recreate the coloring during runtime by inspecting all the messages and injecting color markers where suitable, but that could be very computational heavy, so not sure if it is worth it. It would be nice if api could return the colored original, but that would require saving the logs twice, which is also probably not worth it.

@github-actions github-actions bot added this to the 114th sprint - Tooling team milestone May 14, 2025
@github-actions github-actions bot added t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics. labels May 14, 2025
@Pijukatel
Copy link
Contributor Author

Original log colors are now respected:
image

Thanks a lot @barjin for letting me know about the secret parameter of the log endpoint!

@Pijukatel Pijukatel requested a review from Copilot May 15, 2025 08:54
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new log redirection feature for Actor runs, allowing logs to be streamed and redirected to either a default or a custom logger. Key changes include:

  • New synchronous and asynchronous methods (get_streamed_log) in the Run and Log clients for redirecting logs.
  • Implementation of StreamedLog classes (sync and async) to handle chunked log messages and ensure proper formatting.
  • Updates to the Actor client to support an optional logger parameter for redirecting logs, along with additional tests and dependency updates.

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/unit/test_logging.py Added tests to verify correct log formatting and redirection behavior.
src/apify_client/clients/resource_clients/run.py Introduced get_streamed_log methods for both sync and async clients.
src/apify_client/clients/resource_clients/log.py Updated log retrieval methods with a new raw option and added StreamedLog classes for log streaming.
src/apify_client/clients/resource_clients/actor.py Extended Actor.call method to support log redirection via an optional logger parameter.
src/apify_client/_logging.py Added create_redirect_logger and improved RedirectLogFormatter with color support.
pyproject.toml Added necessary dependencies for color support via colorama.

Have explicit start and stop instead of __call__
@Pijukatel Pijukatel force-pushed the redirected-actor-logs branch from 32def80 to 74595f9 Compare May 15, 2025 09:12
@Pijukatel Pijukatel changed the title Add redirected actor logs feat: Add redirected actor logs May 15, 2025
@Pijukatel Pijukatel requested review from janbuchar, vdusek and MQ37 May 15, 2025 09:23
@Pijukatel
Copy link
Contributor Author

No need to do deep review @MQ37. I think you might be using this functionality soon. Could you please take a look at the use-cases and see if it is convenient to use from your point of view and if there is missing something?
(Timeouts and status messages will be done in separate PR)

@Pijukatel Pijukatel marked this pull request as ready for review May 15, 2025 09:27
Copy link
Contributor

@janbuchar janbuchar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't managed to read the whole thing yet, but here are some comments

Comment on lines 318 to 320
logger: Logger used to redirect logs from the Actor run. By default, it is set to "default" which means that
the default logger will be created and used. Setting `None` will disable any log propagation. Passing
custom logger will redirect logs to the provided logger.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default, it is set to "default" which means that the default logger will be created and used

Um, and what does that actually mean for the caller? Let's try and come up with a better description.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to rephrase it like this:

.... Using "default" literal means that a predefined default logger will be used. ...

@@ -515,6 +548,32 @@ def log(self) -> LogClientAsync:
**self._sub_resource_init_options(resource_path='log'),
)

async def get_streamed_log(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused with the intended usage here. Are you supposed to call this method and then pass the result into an actor.call(logger=...) call?

Copy link
Contributor Author

@Pijukatel Pijukatel May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, actor.call(logger=...) is one group of use-cases that deal with you starting the other actor and then get_streamed_log is called internally.

Using get_streamed_log method directly has different use-case. It is when you want to stream log from already started actor (for example long-running actor in standby)

async with await Actor.new_client().run("some_id").get_streamed_log(from_start=False):
    # Do stuff while logs from other actor are redirceted

@janbuchar janbuchar self-requested a review May 15, 2025 11:12
Copy link

@MQ37 MQ37 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked the example usage and from UX viewpoint this looks good, thank you 👍 @Pijukatel

@Pijukatel Pijukatel force-pushed the redirected-actor-logs branch from df4afce to cba571f Compare May 15, 2025 12:03
Copy link
Contributor

@vdusek vdusek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the Actor call log propagation work recursively? (Actor calls an Actor who calls another Actor...)

@Pijukatel
Copy link
Contributor Author

Does the Actor call log propagation work recursively? (Actor calls an Actor who calls another Actor...)

That is a really good point. It does if the actors were build with version that includes this change. On the other hand the line breaking does not work as expected anymore as the time marker is present for each log (so with two level of log redirection it will be there 3 times and thus there will be extra split)

See example log from this recursive actor:
image

Removing timestamp in redirected logs is probably also not OK as those timestamps are important especially for logs that happened before the streaming started.
That is something to figure out before merging this.

Update test data to properly match the reality with line endings
@Pijukatel
Copy link
Contributor Author

Does the Actor call log propagation work recursively? (Actor calls an Actor who calls another Actor...)

I updated the split pattern and test data to properly respect the real data and now it works. See example of 4 level recursive actor and it's logs:

image

@Pijukatel Pijukatel requested a review from vdusek May 16, 2025 13:33
@Pijukatel Pijukatel force-pushed the redirected-actor-logs branch from c29b896 to 669a749 Compare May 16, 2025 13:46
Comment on lines +320 to +321
mock_api_sync: None, # noqa: ARG001, fixture
propagate_stream_logs: None, # noqa: ARG001, fixture
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we do it better here? Maybe pytest-mark-usefixtures.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@janbuchar I could not find the comment, but I remember we were already discussing it some time ago in another review and you were in favor of # noqa.

I have no problem using this fixture and I am also OK with the # noqa, so if you agree with @vdusek on the preferred way in our code base, I will just use it.

Copy link
Contributor

@janbuchar janbuchar May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm mostly OK with both, but usefixtures does have the drawback of not being applicable to other fixtures...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright, so it's up to you Pepa 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no change then :-)

Copy link
Contributor

@vdusek vdusek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Pijukatel Pijukatel merged commit fd02cd8 into master May 20, 2025
28 checks passed
@Pijukatel Pijukatel deleted the redirected-actor-logs branch May 20, 2025 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Redirect logs from other actors
5 participants