Skip to content
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

Add a generic log retrieval client to Armada Python Client #3892

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/python/armada_client/internal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""DO NOT USE - INTERNAL FACING ONLY - DO NOT USE

Internal modules are *NOT* meant for public consumption. External users of the
armada_client should not use or call any code contained in these modules as
they are unsupported and could change or break at any time.
"""
87 changes: 87 additions & 0 deletions client/python/armada_client/internal/binoculars_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from typing import Optional

import grpc

from armada_client.armada import (
binoculars_pb2,
binoculars_pb2_grpc,
)

from armada_client.k8s.io.api.core.v1 import generated_pb2 as core_v1


def new_binoculars_client(url: str, disable_ssl: bool = False):
"""Constructs and returns a new BinocularsClient object.

:param url: A url specifying the gRPC binoculars endpoint in the format
"host:port".

:return: A new BinocularsClient object.
"""
parts = url.split(":")
if len(parts) != 2:
raise ValueError(f"Could not parse url provided: {url}")

host, port = parts[0], parts[1]
if disable_ssl:
channel = grpc.insecure_channel(f"{host}:{port}")
else:
channel_credentials = grpc.ssl_channel_credentials()
channel = grpc.secure_channel(
f"{host}:{port}",
channel_credentials,
)

client = BinocularsClient(channel)
return (channel, client)


class BinocularsClient:
"""
Client for accessing Armada's Binoculars service over gRPC.

:param channel: gRPC channel used for authentication. See
https://grpc.github.io/grpc/python/grpc.html
for more information.
:return: an Binoculars client instance
"""

def __init__(self, channel):
self.binoculars_stub = binoculars_pb2_grpc.BinocularsStub(channel)

def logs(
self,
job_id: str,
since_time: str,
pod_namespace: Optional[str] = "default",
pod_number: Optional[int] = 0,
log_options: Optional[core_v1.PodLogOptions] = core_v1.PodLogOptions(),
):
"""Retrieve logs for a specific Armada job.

:param job_id: The ID of the job for which to retreieve logs.
:param pod_namespace: The namespace of the pod/job.
:param since_time: If the empty string, retrieves all available logs.
Otherwise, retrieves logs emitted since given timestamp.
:param pod_number: The zero-indexed pod number from which to retrieve
logs. Defaults to zero.
:param log_options: An optional Kubernetes PodLogOptions object.
:return: A LogResponse object.
"""
log_request = binoculars_pb2.LogRequest(
job_id=job_id,
pod_number=pod_number,
pod_namespace=pod_namespace,
since_time=since_time,
log_options=log_options,
)
return self.binoculars_stub.Logs(log_request)

def cordon(self, node_name: str):
"""Send a cordon request for a specific node.

:param node_name: The name of the node.
:return: Empty grpc object.
"""
cordon_request = binoculars_pb2.CordonRequest(node_name=node_name)
return self.binoculars_stub.Cordon(cordon_request)
42 changes: 42 additions & 0 deletions client/python/armada_client/log_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from dataclasses import dataclass
from typing import Optional

from armada_client.internal.binoculars_client import new_binoculars_client


@dataclass
class LogLine:
"""Represents a single line from a log."""

line: str
timestamp: str


class JobLogClient:
"""
Client for retrieving logs for a given job.

:param url: The url to use for retreiving logs.
:param job_id: The ID of the job.
:return: A JobLogClient instance.
"""

def __init__(self, url: str, job_id: str, disable_ssl: bool = False):
self.job_id = job_id
self.url = url
self._channel, self._concrete_client = new_binoculars_client(
self.url, disable_ssl
)

def logs(self, since_time: Optional[str] = ""):
"""Retrieve logs for the job associated with this client.

:param since_time: Logs will be retrieved starting at the time
specified in this str. Must conform to RFC3339 date time format.

:return: A list of LogLine objects.
"""
return [
LogLine(line.line, line.timestamp)
for line in self._concrete_client.logs(self.job_id, since_time).log
]
9 changes: 8 additions & 1 deletion client/python/docs/source/python_armada_client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,11 @@ armada\_client.permissions module
---------------------------------

.. automodule:: armada_client.permissions
:members:
:members:


armada\_client.log_client module
---------------------------------------

.. automodule:: armada_client.log_client
:members:
26 changes: 26 additions & 0 deletions client/python/examples/binoculars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Example script utiltizing JobLogClient."""

import os

from armada_client.log_client import JobLogClient


DISABLE_SSL = os.environ.get("DISABLE_SSL", True)
HOST = os.environ.get("BINOCULARS_SERVER", "localhost")
PORT = os.environ.get("BINOCULARS_PORT", "50053")
JOB_ID = os.environ.get("JOB_ID")


def main():
"""Demonstrate basic use of JobLogClient."""
url = f"{HOST}:{PORT}"
client = JobLogClient(url, JOB_ID, DISABLE_SSL)

log_lines = client.logs()

for line in log_lines:
print(line.line)


if __name__ == "__main__":
main()
20 changes: 18 additions & 2 deletions client/python/tests/unit/server_mock.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from google.protobuf import empty_pb2

from armada_client.armada import (
submit_pb2_grpc,
submit_pb2,
binoculars_pb2,
binoculars_pb2_grpc,
event_pb2,
event_pb2_grpc,
health_pb2,
job_pb2_grpc,
job_pb2,
submit_pb2,
submit_pb2_grpc,
)
from armada_client.armada.job_pb2 import JobRunState
from armada_client.armada.submit_pb2 import JobState
Expand Down Expand Up @@ -149,3 +151,17 @@ def GetJobRunDetails(self, request, context):
for run in request.run_ids
}
)


class BinocularsService(binoculars_pb2_grpc.BinocularsServicer):
def Logs(self, request, context):
return binoculars_pb2.LogResponse(
log=[
binoculars_pb2.LogLine(timestamp="now", line="some log contents!"),
binoculars_pb2.LogLine(timestamp="now", line="some more log contents!"),
binoculars_pb2.LogLine(timestamp="now", line="even more log contents!"),
],
)

def Cordon(self, request, context):
return empty_pb2.Empty()
53 changes: 53 additions & 0 deletions client/python/tests/unit/test_log_clients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from concurrent import futures

import grpc
import pytest

from google.protobuf import empty_pb2

from server_mock import BinocularsService

from armada_client.armada import binoculars_pb2_grpc
from armada_client.internal.binoculars_client import BinocularsClient
from armada_client.log_client import JobLogClient, LogLine


@pytest.fixture(scope="session", autouse=True)
def binoculars_server_mock():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
binoculars_pb2_grpc.add_BinocularsServicer_to_server(BinocularsService(), server)
server.add_insecure_port("[::]:4000")
server.start()

yield
server.stop(False)


channel = grpc.insecure_channel(target="127.0.0.1:4000")
tester = BinocularsClient(
grpc.insecure_channel(
target="127.0.0.1:4000",
options={
"grpc.keepalive_time_ms": 30000,
}.items(),
)
)


def test_logs():
resp = tester.logs("fake-job-id", "fake-namespace", "")
assert len(resp.log) == 3


def test_cordon():
result = tester.cordon("fake-node-name")
assert result == empty_pb2.Empty()


def test_job_log_client():
client = JobLogClient("127.0.0.1:4000", "fake-job-id", True)
log_lines = client.logs()
assert len(log_lines) == 3
for line in log_lines:
assert isinstance(line, LogLine)
assert len(line.line) > 0
56 changes: 56 additions & 0 deletions docs/python_armada_client.md
Original file line number Diff line number Diff line change
Expand Up @@ -654,3 +654,59 @@ Convert this Subject to a grpc Subject.
* **Return type**

armada.submit_pb2.Subject


## armada_client.log_client module


### _class_ armada_client.log_client.JobLogClient(url, job_id, disable_ssl=False)
Client for retrieving logs for a given job.


* **Parameters**


* **url** (*str*) – The url to use for retreiving logs.


* **job_id** (*str*) – The ID of the job.


* **disable_ssl** (*bool*) –



* **Returns**

A JobLogClient instance.



#### logs(since_time='')
Retrieve logs for the job associated with this client.


* **Parameters**

**since_time** (*str** | **None*) – Logs will be retrieved starting at the time
specified in this str. Must conform to RFC3339 date time format.



* **Returns**

A list of LogLine objects.



### _class_ armada_client.log_client.LogLine(line, timestamp)
Represents a single line from a log.


* **Parameters**


* **line** (*str*) –


* **timestamp** (*str*) –
4 changes: 2 additions & 2 deletions scripts/build-python-client.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@

# make the python package armada.client, not pkg.api
mkdir -p proto/armada
cp pkg/api/event.proto pkg/api/submit.proto pkg/api/health.proto pkg/api/job.proto proto/armada
cp pkg/api/event.proto pkg/api/submit.proto pkg/api/health.proto pkg/api/job.proto pkg/api/binoculars/binoculars.proto proto/armada
sed -i 's/\([^\/]\)pkg\/api/\1armada/g' proto/armada/*.proto

# generate python stubs
cd proto
python3 -m grpc_tools.protoc -I. --plugin=protoc-gen-mypy=$(which protoc-gen-mypy) --python_out=../client/python/armada_client --grpc_python_out=../client/python/armada_client --mypy_out=../client/python/armada_client \
google/api/annotations.proto \
google/api/http.proto \
armada/event.proto armada/submit.proto armada/health.proto armada/job.proto \
armada/event.proto armada/submit.proto armada/health.proto armada/job.proto armada/binoculars.proto \
github.com/gogo/protobuf/gogoproto/gogo.proto \
k8s.io/api/core/v1/generated.proto \
k8s.io/apimachinery/pkg/api/resource/generated.proto \
Expand Down
Loading