From 16dbc1d95abfa8f129bd34b86658924bd05ba041 Mon Sep 17 00:00:00 2001 From: Dan Fuchs Date: Thu, 15 Oct 2026 15:54:08 -0500 Subject: [PATCH] feat(logging)!: Use StructuredLogHandler for Cloud Run Jobs by default Use StructuredLogHandler instead of CloudLoggingHandler by default in Cloud Run Jobs. There's no reason that I can think that we can't just log to stdout in Cloud Run Jobs by default like we do in Cloud Run Services and Cloud Run Functions. Starting a background thread for log shipping adds complexity and can cause errors, especiallly when interactiing with other libs like `aiohttp`, leading to errors like this: ``` File "/usr/local/lib/python3.13/site-packages/aiohttp/connector.py", line 379, in __del__ self._loop.call_exception_handler(context) File "/usr/local/lib/python3.13/asyncio/base_events.py", line 1925, in call_exception_handler logger.error('Exception in default exception handler', File "/usr/local/lib/python3.13/logging/__init__.py", line 1554, in error self._log(ERROR, msg, args, **kwargs) File "/usr/local/lib/python3.13/logging/__init__.py", line 1670, in _log self.handle(record) File "/usr/local/lib/python3.13/logging/__init__.py", line 1686, in handle self.callHandlers(record) File "/usr/local/lib/python3.13/logging/__init__.py", line 1746, in callHandlers hdlr.handle(record) File "/usr/local/lib/python3.13/logging/__init__.py", line 1027, in handle self.emit(record) File "/usr/local/lib/python3.13/site-packages/google/cloud/logging_v2/handlers/handlers.py", line 218, in emit self.transport = self._transport_cls( File "/usr/local/lib/python3.13/site-packages/google/cloud/logging_v2/handlers/transports/background_thread.py", line 324, in __init__ self.worker.start() File "/usr/local/lib/python3.13/site-packages/google/cloud/logging_v2/handlers/transports/background_thread.py", line 173, in start self._thread.start() File "/usr/local/lib/python3.13/threading.py", line 976, in start _start_joinable_thread(self._bootstrap, handle=self._handle, PythonFinalizationError: can't create new thread at interpreter shutdown ``` Of course just because *I* can't think of a reason why `CloudLoggingHandler` should be the default doesn't mean there isn't one, and this is a breaking change that could cause problems for folks depending on the existing behavior. Fixes https://github.com/googleapis/google-cloud-python/issues/15407 BREAKING CHANGE: This changes the default log handler for all Cloud Run Jobs --- .../google/cloud/logging_v2/client.py | 7 +++-- .../tests/unit/test_client.py | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/google-cloud-logging/google/cloud/logging_v2/client.py b/packages/google-cloud-logging/google/cloud/logging_v2/client.py index db1b1c8c0380..cf9c5f116383 100644 --- a/packages/google-cloud-logging/google/cloud/logging_v2/client.py +++ b/packages/google-cloud-logging/google/cloud/logging_v2/client.py @@ -58,6 +58,7 @@ _GAE_RESOURCE_TYPE = "gae_app" _GKE_RESOURCE_TYPE = "k8s_container" _GCF_RESOURCE_TYPE = "cloud_function" +_JOB_RESOURCE_TYPE = "cloud_run_job" _RUN_RESOURCE_TYPE = "cloud_run_revision" @@ -380,15 +381,17 @@ def get_default_handler(self, **kw): if isinstance(monitored_resource, Resource): if monitored_resource.type == _GAE_RESOURCE_TYPE: return CloudLoggingHandler(self, resource=monitored_resource, **kw) - elif monitored_resource.type == _GKE_RESOURCE_TYPE: - return StructuredLogHandler(**kw, project_id=self.project) elif monitored_resource.type == _GCF_RESOURCE_TYPE: # Bypassing redirects via __stdout__ ensures structured logging # works robustly on Google Cloud Functions even if stdout is redirected. kw["stream"] = kw.get("stream", sys.__stdout__) return StructuredLogHandler(**kw, project_id=self.project) + elif monitored_resource.type == _GKE_RESOURCE_TYPE: + return StructuredLogHandler(**kw, project_id=self.project) elif monitored_resource.type == _RUN_RESOURCE_TYPE: return StructuredLogHandler(**kw, project_id=self.project) + elif monitored_resource.type == _JOB_RESOURCE_TYPE: + return StructuredLogHandler(**kw, project_id=self.project) return CloudLoggingHandler(self, resource=monitored_resource, **kw) def setup_logging( diff --git a/packages/google-cloud-logging/tests/unit/test_client.py b/packages/google-cloud-logging/tests/unit/test_client.py index e7c1a9c3f0f1..857c6c11c1c5 100644 --- a/packages/google-cloud-logging/tests/unit/test_client.py +++ b/packages/google-cloud-logging/tests/unit/test_client.py @@ -777,6 +777,36 @@ def test_get_default_handler_app_engine(self): self.assertIsInstance(handler, CloudLoggingHandler) + def test_get_default_handler_cloud_run_job(self): + import os + + from google.cloud._testing import _Monkey + + from google.cloud.logging.handlers import StructuredLogHandler + from google.cloud.logging_v2.handlers._monitored_resources import ( + _CLOUD_RUN_EXECUTION_ID, + _CLOUD_RUN_JOB_ID, + _CLOUD_RUN_TASK_ATTEMPT, + _CLOUD_RUN_TASK_INDEX, + ) + + credentials = _make_credentials() + client = self._make_one( + project=self.PROJECT, credentials=credentials, _use_grpc=False + ) + + cloud_run_job_env_vars = { + _CLOUD_RUN_JOB_ID: "TRUE", + _CLOUD_RUN_EXECUTION_ID: "TRUE", + _CLOUD_RUN_TASK_INDEX: "0", + _CLOUD_RUN_TASK_ATTEMPT: "0", + } + + with _Monkey(os, environ=cloud_run_job_env_vars): + handler = client.get_default_handler() + + self.assertIsInstance(handler, StructuredLogHandler) + def test_get_default_handler_container_engine(self): from google.cloud.logging.handlers import StructuredLogHandler