Skip to content

Commit 8854616

Browse files
feat: enable retrieval of service logs (via API) (#1515)
Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com>
1 parent 07b3f54 commit 8854616

File tree

4 files changed

+149
-1
lines changed

4 files changed

+149
-1
lines changed

doc/changelog.d/1515.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
enable retrieval of service logs (via API)

src/ansys/geometry/core/connection/client.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
from typing import Optional
2929
import warnings
3030

31+
from ansys.geometry.core.errors import protect_grpc
32+
3133
# TODO: Remove this context and filter once the protobuf UserWarning issue is downgraded to INFO
3234
# https://github.com/grpc/grpc/issues/37609
3335
with warnings.catch_warnings():
@@ -43,7 +45,12 @@
4345
from beartype import beartype as check_input_types
4446
import semver
4547

46-
from ansys.api.dbu.v0.admin_pb2 import BackendType as GRPCBackendType
48+
from ansys.api.dbu.v0.admin_pb2 import (
49+
BackendType as GRPCBackendType,
50+
LogsRequest,
51+
LogsTarget,
52+
PeriodType,
53+
)
4754
from ansys.api.dbu.v0.admin_pb2_grpc import AdminStub
4855
from ansys.geometry.core.connection.backend import BackendType
4956
from ansys.geometry.core.connection.defaults import DEFAULT_HOST, DEFAULT_PORT, MAX_MESSAGE_LENGTH
@@ -327,3 +334,71 @@ def target(self) -> str:
327334
def get_name(self) -> str:
328335
"""Get the target name of the connection."""
329336
return self._target
337+
338+
@check_input_types
339+
@protect_grpc
340+
def _get_service_logs(
341+
self,
342+
all_logs: bool = False,
343+
dump_to_file: bool = False,
344+
logs_folder: str | Path | None = None,
345+
) -> str | dict[str, str] | Path:
346+
"""Get the service logs.
347+
348+
Parameters
349+
----------
350+
all_logs : bool, default: False
351+
Flag indicating whether all logs should be retrieved. By default,
352+
only the current logs are retrieved.
353+
dump_to_file : bool, default: False
354+
Flag indicating whether the logs should be dumped to a file.
355+
By default, the logs are not dumped to a file.
356+
logs_folder : str, Path or None, default: None
357+
Name of the folder where the logs should be dumped. This parameter
358+
is only used if the ``dump_to_file`` parameter is set to ``True``.
359+
360+
Returns
361+
-------
362+
str
363+
Service logs as a string. This is returned if the ``dump_to_file`` parameter
364+
is set to ``False``.
365+
dict[str, str]
366+
Dictionary containing the logs. The keys are the logs names,
367+
and the values are the logs as strings. This is returned if the ``all_logs``
368+
parameter is set to ``True`` and the ``dump_to_file`` parameter
369+
is set to ``False``.
370+
Path
371+
Path to the folder containing the logs (if the ``all_logs``
372+
parameter is set to ``True``) or the path to the log file (if only
373+
the current logs are retrieved). The ``dump_to_file`` parameter
374+
must be set to ``True``.
375+
"""
376+
request = LogsRequest(
377+
target=LogsTarget.CLIENT,
378+
period_type=PeriodType.CURRENT if not all_logs else PeriodType.ALL,
379+
null_path=None,
380+
null_period=None,
381+
)
382+
logs_generator = self._admin_stub.GetLogs(request)
383+
logs: dict[str, str] = {}
384+
385+
for chunk in logs_generator:
386+
if chunk.log_name not in logs:
387+
logs[chunk.log_name] = ""
388+
logs[chunk.log_name] += chunk.log_chunk.decode()
389+
390+
# Let's handle the various scenarios...
391+
if not dump_to_file:
392+
return logs if all_logs else next(iter(logs.values()))
393+
else:
394+
if logs_folder is None:
395+
logs_folder = Path.cwd()
396+
elif isinstance(logs_folder, str):
397+
logs_folder = Path(logs_folder)
398+
399+
logs_folder.mkdir(parents=True, exist_ok=True)
400+
for log_name, log_content in logs.items():
401+
with (logs_folder / log_name).open("w") as f:
402+
f.write(log_content)
403+
404+
return (logs_folder / log_name) if len(logs) == 1 else logs_folder

src/ansys/geometry/core/modeler.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,44 @@ def prepare_tools(self) -> PrepareTools:
497497
def measurement_tools(self) -> MeasurementTools:
498498
"""Access to measurement tools."""
499499
return self._measurement_tools
500+
501+
@min_backend_version(25, 1, 0)
502+
def get_service_logs(
503+
self,
504+
all_logs: bool = False,
505+
dump_to_file: bool = False,
506+
logs_folder: str | Path | None = None,
507+
) -> str | dict[str, str] | Path:
508+
"""Get the service logs.
509+
510+
Parameters
511+
----------
512+
all_logs : bool, default: False
513+
Flag indicating whether all logs should be retrieved. By default,
514+
only the current logs are retrieved.
515+
dump_to_file : bool, default: False
516+
Flag indicating whether the logs should be dumped to a file.
517+
By default, the logs are not dumped to a file.
518+
logs_folder : str, Path or None, default: None
519+
Name of the folder where the logs should be dumped. This parameter
520+
is only used if the ``dump_to_file`` parameter is set to ``True``.
521+
522+
Returns
523+
-------
524+
str
525+
Service logs as a string. This is returned if the ``dump_to_file`` parameter
526+
is set to ``False``.
527+
dict[str, str]
528+
Dictionary containing the logs. The keys are the logs names,
529+
and the values are the logs as strings. This is returned if the ``all_logs``
530+
parameter is set to ``True`` and the ``dump_to_file`` parameter
531+
is set to ``False``.
532+
Path
533+
Path to the folder containing the logs (if the ``all_logs``
534+
parameter is set to ``True``) or the path to the log file (if only
535+
the current logs are retrieved). The ``dump_to_file`` parameter
536+
must be set to ``True``.
537+
"""
538+
return self.client._get_service_logs(
539+
all_logs=all_logs, dump_to_file=dump_to_file, logs_folder=logs_folder
540+
)

tests/integration/test_client.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
# SOFTWARE.
2222
"""Test basic client connection."""
2323

24+
from pathlib import Path
25+
2426
from grpc import insecure_channel
2527
import pytest
2628

@@ -65,3 +67,32 @@ def test_client_close(client: GrpcClient):
6567
assert not client.healthy
6668
assert "Closed" in str(client)
6769
assert client.target() == ""
70+
71+
72+
def test_client_get_service_logs(client: GrpcClient):
73+
"""Test the retrieval of the service logs."""
74+
# Low level call
75+
logs = client._get_service_logs()
76+
assert isinstance(logs, str)
77+
assert logs # is not empty
78+
79+
# Let's request them again on file dump
80+
logs_folder = str(Path(__file__).parent / "logs")
81+
logs_file_dump = client._get_service_logs(dump_to_file=True, logs_folder=logs_folder)
82+
assert logs_file_dump.exists()
83+
84+
# Do not provide a folder
85+
logs_file_dump = client._get_service_logs(dump_to_file=True)
86+
assert logs_file_dump.exists()
87+
logs_file_dump.unlink() # Delete the file
88+
89+
# Let's request all logs now
90+
logs_all = client._get_service_logs(all_logs=True)
91+
assert isinstance(logs_all, dict)
92+
assert logs_all # is not empty
93+
94+
# Let's do the same directly from a Modeler object
95+
modeler = Modeler(channel=client.channel)
96+
logs_modeler = modeler.get_service_logs()
97+
assert isinstance(logs_modeler, str)
98+
assert logs_modeler # is not empty

0 commit comments

Comments
 (0)