Skip to content

Commit 0644260

Browse files
Allow an action to get a copy of the logs raised during the action.
1 parent 51d2349 commit 0644260

File tree

3 files changed

+52
-3
lines changed

3 files changed

+52
-3
lines changed

src/labthings_fastapi/actions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
from .base_descriptor import BaseDescriptor
4545
from .logs import add_thing_log_destination
4646
from .utilities import model_to_dict, wrap_plain_types_in_rootmodel
47-
from .invocations import InvocationModel, InvocationStatus, LogRecordModel
47+
from .invocations import InvocationModel, InvocationStatus
4848
from .dependencies.invocation import NonWarningInvocationID
4949
from .exceptions import (
5050
InvocationCancelledError,
@@ -154,7 +154,7 @@ def output(self) -> Any:
154154
return self._return_value
155155

156156
@property
157-
def log(self) -> list[LogRecordModel]:
157+
def log(self) -> list[logging.LogRecord]:
158158
"""A list of log items generated by the Action."""
159159
with self._status_lock:
160160
return list(self._log)

src/labthings_fastapi/thing.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from .websockets import websocket_endpoint
3131
from .exceptions import PropertyNotObservableError
3232
from .thing_server_interface import ThingServerInterface
33-
33+
from .invocation_contexts import get_invocation_id
3434

3535
if TYPE_CHECKING:
3636
from .server import ThingServer
@@ -383,3 +383,23 @@ def observe_action(self, action_name: str, stream: ObjectSendStream) -> None:
383383
raise KeyError(f"{action_name} is not an LabThings Action")
384384
observers = action._observers_set(self)
385385
observers.add(stream)
386+
387+
def get_logs(self) -> list[logging.LogRecord]:
388+
"""Get the log records for an on going action.
389+
390+
This is useful if an action wishes to save its logs alongside any data.
391+
392+
Note that only the last 1000 logs are returned so for long running tasks that
393+
log frequently this may want to be read periodically.
394+
395+
This will error if it is called outside an action invocation.
396+
397+
:return: a list of all logs from this action.
398+
"""
399+
inv_id = get_invocation_id()
400+
server = self._thing_server_interface._server()
401+
if server is None:
402+
raise RuntimeError("Could not get server from thing_server_interface")
403+
action_manager = server.action_manager
404+
this_invocation = action_manager.get_invocation(inv_id)
405+
return this_invocation.log

tests/test_logs.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from types import EllipsisType
1111
import pytest
1212
from uuid import UUID, uuid4
13+
from fastapi.testclient import TestClient
1314
from labthings_fastapi import logs
1415
from labthings_fastapi.invocation_contexts import (
1516
fake_invocation_context,
@@ -19,13 +20,29 @@
1920
from labthings_fastapi.exceptions import LogConfigurationError
2021
from labthings_fastapi.testing import create_thing_without_server
2122

23+
from .temp_client import poll_task
24+
2225

2326
class ThingThatLogs(lt.Thing):
2427
@lt.action
2528
def log_a_message(self, msg: str):
2629
"""Log a message to the thing's logger."""
2730
self.logger.info(msg)
2831

32+
@lt.action
33+
def log_and_capture(self, msg: str) -> str:
34+
"""Log a message to the thing's logger."""
35+
self.logger.info(msg)
36+
self.logger.warning(msg)
37+
self.logger.error(msg)
38+
logs = self.get_logs()
39+
logging_str = ""
40+
for record in logs:
41+
level = record.levelname
42+
msg = record.getMessage()
43+
logging_str += f"[{level}] {msg}\n"
44+
return logging_str
45+
2946

3047
def reset_thing_logger():
3148
"""Remove all handlers from the THING_LOGGER to reset it."""
@@ -176,3 +193,15 @@ def test_add_thing_log_destination():
176193
thing.log_a_message("Test Message.")
177194
assert len(dest) == 1
178195
assert dest[0].getMessage() == "Test Message."
196+
197+
198+
def test_action_can_get_logs():
199+
"""Check that an action can get a copy of its own logs."""
200+
server = lt.ThingServer({"logging_thing": ThingThatLogs})
201+
with TestClient(server.app) as client:
202+
response = client.post("/logging_thing/log_and_capture", json={"msg": "foobar"})
203+
response.raise_for_status()
204+
invocation = poll_task(client, response.json())
205+
assert invocation["status"] == "completed"
206+
expected_message = "[INFO] foobar\n[WARNING] foobar\n[ERROR] foobar\n"
207+
assert invocation["output"] == expected_message

0 commit comments

Comments
 (0)