-
Notifications
You must be signed in to change notification settings - Fork 17.5k
Fix four bugs in StackdriverTaskHandler #13784
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,6 +99,7 @@ def __init__( | |
| self.resource: Resource = resource | ||
| self.labels: Optional[Dict[str, str]] = labels | ||
| self.task_instance_labels: Optional[Dict[str, str]] = {} | ||
| self.task_instance_hostname = 'default-hostname' | ||
|
|
||
| @cached_property | ||
| def _client(self) -> gcp_logging.Client: | ||
|
|
@@ -146,10 +147,11 @@ def set_context(self, task_instance: TaskInstance) -> None: | |
| :type task_instance: :class:`airflow.models.TaskInstance` | ||
| """ | ||
| self.task_instance_labels = self._task_instance_to_labels(task_instance) | ||
| self.task_instance_hostname = task_instance.hostname | ||
|
|
||
| def read( | ||
| self, task_instance: TaskInstance, try_number: Optional[int] = None, metadata: Optional[Dict] = None | ||
| ) -> Tuple[List[str], List[Dict]]: | ||
| ) -> Tuple[List[Tuple[Tuple[str, str]]], List[Dict[str, str]]]: | ||
| """ | ||
| Read logs of given task instance from Stackdriver logging. | ||
|
|
||
|
|
@@ -160,12 +162,14 @@ def read( | |
| :type try_number: Optional[int] | ||
| :param metadata: log metadata. It is used for steaming log reading and auto-tailing. | ||
| :type metadata: Dict | ||
| :return: a tuple of list of logs and list of metadata | ||
| :rtype: Tuple[List[str], List[Dict]] | ||
| :return: a tuple of ( | ||
| list of (one element tuple with two element tuple - hostname and logs) | ||
| and list of metadata) | ||
| :rtype: Tuple[List[Tuple[Tuple[str, str]]], List[Dict[str, str]]] | ||
| """ | ||
| if try_number is not None and try_number < 1: | ||
| logs = [f"Error fetching the logs. Try number {try_number} is invalid."] | ||
| return logs, [{"end_of_log": "true"}] | ||
| logs = f"Error fetching the logs. Try number {try_number} is invalid." | ||
| return [((self.task_instance_hostname, logs),)], [{"end_of_log": "true"}] | ||
|
|
||
| if not metadata: | ||
| metadata = {} | ||
|
|
@@ -188,7 +192,7 @@ def read( | |
| if next_page_token: | ||
| new_metadata['next_page_token'] = next_page_token | ||
|
|
||
| return [messages], [new_metadata] | ||
| return [((self.task_instance_hostname, messages),)], [new_metadata] | ||
|
|
||
| def _prepare_log_filter(self, ti_labels: Dict[str, str]) -> str: | ||
| """ | ||
|
|
@@ -252,6 +256,8 @@ def _read_logs( | |
| log_filter=log_filter, page_token=next_page_token | ||
| ) | ||
| messages.append(new_messages) | ||
| if not messages: | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stackdriver sometimes falls into an endless loop of blank pages. |
||
| break | ||
|
|
||
| end_of_log = True | ||
| next_page_token = None | ||
|
|
@@ -271,7 +277,9 @@ def _read_single_logs_page(self, log_filter: str, page_token: Optional[str] = No | |
| :return: Downloaded logs and next page token | ||
| :rtype: Tuple[str, str] | ||
| """ | ||
| entries = self._client.list_entries(filter_=log_filter, page_token=page_token) | ||
| entries = self._client.list_entries( | ||
| filter_=log_filter, page_token=page_token, order_by='timestamp asc', page_size=1000 | ||
| ) | ||
| page = next(entries.pages) | ||
| next_page_token = entries.next_page_token | ||
| messages = [] | ||
|
|
@@ -331,3 +339,6 @@ def get_external_log_url(self, task_instance: TaskInstance, try_number: int) -> | |
|
|
||
| url = f"{self.LOG_VIEWER_BASE_URL}?{urlencode(url_query_string)}" | ||
| return url | ||
|
|
||
| def close(self) -> None: | ||
| self._transport.flush() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| import contextlib | ||
| import importlib | ||
| import io | ||
| import logging | ||
| import os | ||
| import unittest | ||
| from unittest import mock | ||
|
|
@@ -129,6 +130,8 @@ def test_should_read_logging_configuration(self): | |
| assert "stackdriver" in text | ||
|
|
||
| def tearDown(self) -> None: | ||
| for handler_ref in logging._handlerList[:]: | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this test, we used the StackdriverTaskHandler which tries to connect to GCP in the |
||
| logging._removeHandlerRef(handler_ref) | ||
| importlib.reload(airflow_local_settings) | ||
| configure_logging() | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,10 +35,21 @@ def _create_list_response(messages, token): | |
| return mock.MagicMock(pages=(n for n in [page]), next_page_token=token) | ||
|
|
||
|
|
||
| def _remove_stackdriver_handlers(): | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is another small fix. An error is generated when exiting the process, but it did not cause any errors, but only noise in the log. |
||
| for handler_ref in reversed(logging._handlerList[:]): | ||
| handler = handler_ref() | ||
| if not isinstance(handler, StackdriverTaskHandler): | ||
| continue | ||
| logging._removeHandlerRef(handler_ref) | ||
| del handler | ||
|
|
||
|
|
||
| class TestStackdriverLoggingHandlerStandalone(unittest.TestCase): | ||
| @mock.patch('airflow.providers.google.cloud.log.stackdriver_task_handler.get_credentials_and_project_id') | ||
| @mock.patch('airflow.providers.google.cloud.log.stackdriver_task_handler.gcp_logging.Client') | ||
| def test_should_pass_message_to_client(self, mock_client, mock_get_creds_and_project_id): | ||
| self.addCleanup(_remove_stackdriver_handlers) | ||
|
|
||
| mock_get_creds_and_project_id.return_value = ('creds', 'project_id') | ||
|
|
||
| transport_type = mock.MagicMock() | ||
|
|
@@ -69,6 +80,7 @@ def setUp(self) -> None: | |
| self.ti.try_number = 1 | ||
| self.ti.state = State.RUNNING | ||
| self.addCleanup(self.dag.clear) | ||
| self.addCleanup(_remove_stackdriver_handlers) | ||
|
|
||
| @mock.patch('airflow.providers.google.cloud.log.stackdriver_task_handler.get_credentials_and_project_id') | ||
| @mock.patch('airflow.providers.google.cloud.log.stackdriver_task_handler.gcp_logging.Client') | ||
|
|
@@ -128,14 +140,18 @@ def test_should_read_logs_for_all_try(self, mock_client, mock_get_creds_and_proj | |
|
|
||
| logs, metadata = self.stackdriver_task_handler.read(self.ti) | ||
| mock_client.return_value.list_entries.assert_called_once_with( | ||
| filter_='resource.type="global"\n' | ||
| 'logName="projects/asf-project/logs/airflow"\n' | ||
| 'labels.task_id="task_for_testing_file_log_handler"\n' | ||
| 'labels.dag_id="dag_for_testing_file_task_handler"\n' | ||
| 'labels.execution_date="2016-01-01T00:00:00+00:00"', | ||
| filter_=( | ||
| 'resource.type="global"\n' | ||
| 'logName="projects/asf-project/logs/airflow"\n' | ||
| 'labels.task_id="task_for_testing_file_log_handler"\n' | ||
| 'labels.dag_id="dag_for_testing_file_task_handler"\n' | ||
| 'labels.execution_date="2016-01-01T00:00:00+00:00"' | ||
| ), | ||
| order_by='timestamp asc', | ||
| page_size=1000, | ||
| page_token=None, | ||
| ) | ||
| assert ['MSG1\nMSG2'] == logs | ||
| assert [(('default-hostname', 'MSG1\nMSG2'),)] == logs | ||
| assert [{'end_of_log': True}] == metadata | ||
|
|
||
| @mock.patch('airflow.providers.google.cloud.log.stackdriver_task_handler.get_credentials_and_project_id') | ||
|
|
@@ -149,14 +165,18 @@ def test_should_read_logs_for_task_with_quote(self, mock_client, mock_get_creds_ | |
| self.ti.task_id = "K\"OT" | ||
| logs, metadata = self.stackdriver_task_handler.read(self.ti) | ||
| mock_client.return_value.list_entries.assert_called_once_with( | ||
| filter_='resource.type="global"\n' | ||
| 'logName="projects/asf-project/logs/airflow"\n' | ||
| 'labels.task_id="K\\"OT"\n' | ||
| 'labels.dag_id="dag_for_testing_file_task_handler"\n' | ||
| 'labels.execution_date="2016-01-01T00:00:00+00:00"', | ||
| filter_=( | ||
| 'resource.type="global"\n' | ||
| 'logName="projects/asf-project/logs/airflow"\n' | ||
| 'labels.task_id="K\\"OT"\n' | ||
| 'labels.dag_id="dag_for_testing_file_task_handler"\n' | ||
| 'labels.execution_date="2016-01-01T00:00:00+00:00"' | ||
| ), | ||
| order_by='timestamp asc', | ||
| page_size=1000, | ||
| page_token=None, | ||
| ) | ||
| assert ['MSG1\nMSG2'] == logs | ||
| assert [(('default-hostname', 'MSG1\nMSG2'),)] == logs | ||
| assert [{'end_of_log': True}] == metadata | ||
|
|
||
| @mock.patch('airflow.providers.google.cloud.log.stackdriver_task_handler.get_credentials_and_project_id') | ||
|
|
@@ -170,15 +190,19 @@ def test_should_read_logs_for_single_try(self, mock_client, mock_get_creds_and_p | |
|
|
||
| logs, metadata = self.stackdriver_task_handler.read(self.ti, 3) | ||
| mock_client.return_value.list_entries.assert_called_once_with( | ||
| filter_='resource.type="global"\n' | ||
| 'logName="projects/asf-project/logs/airflow"\n' | ||
| 'labels.task_id="task_for_testing_file_log_handler"\n' | ||
| 'labels.dag_id="dag_for_testing_file_task_handler"\n' | ||
| 'labels.execution_date="2016-01-01T00:00:00+00:00"\n' | ||
| 'labels.try_number="3"', | ||
| filter_=( | ||
| 'resource.type="global"\n' | ||
| 'logName="projects/asf-project/logs/airflow"\n' | ||
| 'labels.task_id="task_for_testing_file_log_handler"\n' | ||
| 'labels.dag_id="dag_for_testing_file_task_handler"\n' | ||
| 'labels.execution_date="2016-01-01T00:00:00+00:00"\n' | ||
| 'labels.try_number="3"' | ||
| ), | ||
| order_by='timestamp asc', | ||
| page_size=1000, | ||
| page_token=None, | ||
| ) | ||
| assert ['MSG1\nMSG2'] == logs | ||
| assert [(('default-hostname', 'MSG1\nMSG2'),)] == logs | ||
| assert [{'end_of_log': True}] == metadata | ||
|
|
||
| @mock.patch('airflow.providers.google.cloud.log.stackdriver_task_handler.get_credentials_and_project_id') | ||
|
|
@@ -190,14 +214,18 @@ def test_should_read_logs_with_pagination(self, mock_client, mock_get_creds_and_ | |
| ] | ||
| mock_get_creds_and_project_id.return_value = ('creds', 'project_id') | ||
| logs, metadata1 = self.stackdriver_task_handler.read(self.ti, 3) | ||
| mock_client.return_value.list_entries.assert_called_once_with(filter_=mock.ANY, page_token=None) | ||
| assert ['MSG1\nMSG2'] == logs | ||
| mock_client.return_value.list_entries.assert_called_once_with( | ||
| filter_=mock.ANY, order_by='timestamp asc', page_size=1000, page_token=None | ||
| ) | ||
| assert [(('default-hostname', 'MSG1\nMSG2'),)] == logs | ||
| assert [{'end_of_log': False, 'next_page_token': 'TOKEN1'}] == metadata1 | ||
|
|
||
| mock_client.return_value.list_entries.return_value.next_page_token = None | ||
| logs, metadata2 = self.stackdriver_task_handler.read(self.ti, 3, metadata1[0]) | ||
| mock_client.return_value.list_entries.assert_called_with(filter_=mock.ANY, page_token="TOKEN1") | ||
| assert ['MSG3\nMSG4'] == logs | ||
| mock_client.return_value.list_entries.assert_called_with( | ||
| filter_=mock.ANY, order_by='timestamp asc', page_size=1000, page_token="TOKEN1" | ||
| ) | ||
| assert [(('default-hostname', 'MSG3\nMSG4'),)] == logs | ||
| assert [{'end_of_log': True}] == metadata2 | ||
|
|
||
| @mock.patch('airflow.providers.google.cloud.log.stackdriver_task_handler.get_credentials_and_project_id') | ||
|
|
@@ -211,7 +239,7 @@ def test_should_read_logs_with_download(self, mock_client, mock_get_creds_and_pr | |
|
|
||
| logs, metadata1 = self.stackdriver_task_handler.read(self.ti, 3, {'download_logs': True}) | ||
|
|
||
| assert ['MSG1\nMSG2\nMSG3\nMSG4'] == logs | ||
| assert [(('default-hostname', 'MSG1\nMSG2\nMSG3\nMSG4'),)] == logs | ||
| assert [{'end_of_log': True}] == metadata1 | ||
|
|
||
| @mock.patch('airflow.providers.google.cloud.log.stackdriver_task_handler.get_credentials_and_project_id') | ||
|
|
@@ -240,17 +268,21 @@ def test_should_read_logs_with_custom_resources(self, mock_client, mock_get_cred | |
|
|
||
| logs, metadata = self.stackdriver_task_handler.read(self.ti) | ||
| mock_client.return_value.list_entries.assert_called_once_with( | ||
| filter_='resource.type="cloud_composer_environment"\n' | ||
| 'logName="projects/asf-project/logs/airflow"\n' | ||
| 'resource.labels."environment.name"="test-instancce"\n' | ||
| 'resource.labels.location="europpe-west-3"\n' | ||
| 'resource.labels.project_id="asf-project"\n' | ||
| 'labels.task_id="task_for_testing_file_log_handler"\n' | ||
| 'labels.dag_id="dag_for_testing_file_task_handler"\n' | ||
| 'labels.execution_date="2016-01-01T00:00:00+00:00"', | ||
| filter_=( | ||
| 'resource.type="cloud_composer_environment"\n' | ||
| 'logName="projects/asf-project/logs/airflow"\n' | ||
| 'resource.labels."environment.name"="test-instancce"\n' | ||
| 'resource.labels.location="europpe-west-3"\n' | ||
| 'resource.labels.project_id="asf-project"\n' | ||
| 'labels.task_id="task_for_testing_file_log_handler"\n' | ||
| 'labels.dag_id="dag_for_testing_file_task_handler"\n' | ||
| 'labels.execution_date="2016-01-01T00:00:00+00:00"' | ||
| ), | ||
| order_by='timestamp asc', | ||
| page_size=1000, | ||
| page_token=None, | ||
| ) | ||
| assert ['TEXT\nTEXT'] == logs | ||
| assert [(('default-hostname', 'TEXT\nTEXT'),)] == logs | ||
| assert [{'end_of_log': True}] == metadata | ||
|
|
||
| @mock.patch('airflow.providers.google.cloud.log.stackdriver_task_handler.get_credentials_and_project_id') | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed... interesting way of reurning data :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed...
List[Tuple[Tuple[str, List]]]😄There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tuple[List[Tuple[Tuple[str, str]]], List[Dict[str, str]]]