Skip to content

Commit 6e146cc

Browse files
prevent duplicate logs in cloudrun job (datacommonsorg#1478)
1 parent e963225 commit 6e146cc

5 files changed

Lines changed: 96 additions & 7 deletions

File tree

import-automation/executor/main.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
sys.path.append(os.path.join(REPO_DIR, 'util'))
3636

3737
from log_util import log_metric, configure_cloud_logging
38+
from cloudrun_util import running_on_cloudrun
3839

3940
FLAGS = flags.FLAGS
4041
flags.DEFINE_string('import_name', '', 'Absoluate import name.')
@@ -101,7 +102,11 @@ def run_import_job(absolute_import_name: str, import_config: str):
101102

102103

103104
def main(_):
104-
if FLAGS.enable_cloud_logging:
105+
running_on_cloudrun_result = running_on_cloudrun()
106+
if running_on_cloudrun_result:
107+
logging.info("Running under Cloud Run detected.")
108+
109+
if FLAGS.enable_cloud_logging or running_on_cloudrun_result:
105110
configure_cloud_logging()
106111
logging.info("Google Cloud Logging configured.")
107112
return run_import_job(FLAGS.import_name, FLAGS.import_config)

tools/statvar_importer/stat_var_processor.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
sys.path.append(os.path.join(_SCRIPT_DIR, 'schema'))
6767

6868
import eval_functions
69+
from log_util import configure_cloud_logging
70+
from cloudrun_util import running_on_cloudrun
6971
from utils import (capitalize_first_char, is_place_dcid,
7072
get_observation_date_format, get_observation_period_for_date,
7173
pvs_has_any_prop, str_from_number, prepare_input_data)
@@ -2788,6 +2790,14 @@ def process(
27882790

27892791

27902792
def main(_):
2793+
# Configure cloud logging if running on CloudRun
2794+
if running_on_cloudrun():
2795+
logging.info("Running under Cloud Run detected.")
2796+
configure_cloud_logging()
2797+
logging.info("Google Cloud Logging configured.")
2798+
else:
2799+
logging.info("Not running under Cloud Run")
2800+
27912801
# uncomment to run pprof
27922802
# start_pprof_server(port=8123)
27932803

util/cloudrun_util.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Utility functions for Cloud Run environment."""
15+
16+
import os
17+
18+
19+
def running_on_cloudrun() -> bool:
20+
"""Check if running on Cloud Run.
21+
22+
Returns:
23+
bool: True if running on Cloud Run services, False otherwise.
24+
"""
25+
return bool(os.getenv('K_SERVICE'))

util/cloudrun_util_test.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Tests for util.cloudrun_util module."""
15+
16+
import os
17+
import unittest
18+
from unittest import mock
19+
20+
from util import cloudrun_util
21+
22+
23+
class CloudRunUtilTest(unittest.TestCase):
24+
"""Tests for Cloud Run utility functions."""
25+
26+
def test_running_on_cloudrun_when_k_service_is_set(self):
27+
"""Test that running_on_cloudrun returns True when K_SERVICE is set."""
28+
with mock.patch.dict(os.environ, {'K_SERVICE': 'my-service'}):
29+
self.assertTrue(cloudrun_util.running_on_cloudrun())
30+
31+
def test_running_on_cloudrun_when_k_service_is_not_set(self):
32+
"""Test that running_on_cloudrun returns False when K_SERVICE is not set."""
33+
with mock.patch.dict(os.environ, {}, clear=True):
34+
# Ensure K_SERVICE is not in environment
35+
if 'K_SERVICE' in os.environ:
36+
del os.environ['K_SERVICE']
37+
self.assertFalse(cloudrun_util.running_on_cloudrun())
38+
39+
def test_running_on_cloudrun_with_empty_k_service_value(self):
40+
"""Test that running_on_cloudrun returns False when K_SERVICE is empty."""
41+
with mock.patch.dict(os.environ, {'K_SERVICE': ''}):
42+
self.assertFalse(cloudrun_util.running_on_cloudrun())

util/log_util.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
"""Utility functions for logging."""
1515

1616
import json
17+
import logging
1718
import google.cloud.logging
19+
from absl import logging as absl_logging
1820

1921

2022
def log_struct(level: str, message: str, labels: dict):
@@ -67,12 +69,17 @@ def log_metric(log_type: str, level: str, message: str, metric_labels: dict):
6769
def configure_cloud_logging():
6870
"""Configure Google Cloud Logging handler for structured logging with proper severity.
6971
70-
Handles both standard Python logging and absl logging since absl uses Python's
71-
logging system internally. Maps log levels to GCP severity (INFO→INFO, ERROR→ERROR).
72-
73-
Note: Prefer calling this after app.run() begins so that absl's logging handlers
74-
are already set up. Both handlers will coexist - ABSLHandler for console output
75-
and CloudLoggingHandler for structured logs with proper severity.
72+
Removes ABSL handler if present to prevent log duplication between stderr and Cloud Logging.
73+
This ensures logs appear only in Cloud Logging with proper severity levels.
7674
"""
75+
# Remove ABSL handler if present to prevent duplication
76+
absl_handler = absl_logging.get_absl_handler()
77+
if absl_handler in logging.root.handlers:
78+
logging.root.handlers.remove(absl_handler)
79+
logging.info(
80+
"ABSL handler found and removed to prevent log duplication")
81+
else:
82+
logging.info("ABSL handler not found in root handlers")
83+
7784
client = google.cloud.logging.Client()
7885
client.setup_logging()

0 commit comments

Comments
 (0)