Skip to content

Commit ad3406d

Browse files
asrinivadi
andauthored
Add backwards-compatible logging for GCF Python 3.7 (GoogleCloudPlatform#107)
* Add backwards-compatible logging for GCF Python 3.7 * Reformatted * Update src/functions_framework/__init__.py Co-authored-by: Dustin Ingram <[email protected]> * Restructure logging to better fit legacy behavior * Modify write behavior to account for newlines * Update LogHandler to use io.TextIOWrapper * Simplify write method * Update src/functions_framework/__init__.py Co-authored-by: Dustin Ingram <[email protected]> Co-authored-by: Dustin Ingram <[email protected]>
1 parent 147a488 commit ad3406d

File tree

4 files changed

+115
-0
lines changed

4 files changed

+115
-0
lines changed

conftest.py

+16
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import logging
1516
import os
17+
import sys
18+
19+
from importlib import reload
1620

1721
import pytest
1822

@@ -26,3 +30,15 @@ def isolate_environment():
2630
finally:
2731
os.environ.clear()
2832
os.environ.update(_environ)
33+
34+
35+
@pytest.fixture(scope="function", autouse=True)
36+
def isolate_logging():
37+
"Ensure any changes to logging are isolated to individual tests" ""
38+
try:
39+
yield
40+
finally:
41+
sys.stdout = sys.__stdout__
42+
sys.stderr = sys.__stderr__
43+
logging.shutdown()
44+
reload(logging)

src/functions_framework/__init__.py

+25
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import functools
1616
import importlib.util
17+
import io
1718
import json
1819
import os.path
1920
import pathlib
@@ -65,6 +66,19 @@ def __init__(
6566
self.data = data
6667

6768

69+
class _LoggingHandler(io.TextIOWrapper):
70+
"""Logging replacement for stdout and stderr in GCF Python 3.7."""
71+
72+
def __init__(self, level, stderr=sys.stderr):
73+
io.TextIOWrapper.__init__(self, io.StringIO(), encoding=stderr.encoding)
74+
self.level = level
75+
self.stderr = stderr
76+
77+
def write(self, out):
78+
payload = dict(severity=self.level, message=out.rstrip("\n"))
79+
return self.stderr.write(json.dumps(payload) + "\n")
80+
81+
6882
def _http_view_func_wrapper(function, request):
6983
def view_func(path):
7084
return function(request._get_current_object())
@@ -221,6 +235,17 @@ def handle_none(rv):
221235

222236
app.make_response = handle_none
223237

238+
# Handle log severity backwards compatibility
239+
import logging # isort:skip
240+
241+
logging.info = _LoggingHandler("INFO", sys.stderr).write
242+
logging.warn = _LoggingHandler("ERROR", sys.stderr).write
243+
logging.warning = _LoggingHandler("ERROR", sys.stderr).write
244+
logging.error = _LoggingHandler("ERROR", sys.stderr).write
245+
logging.critical = _LoggingHandler("ERROR", sys.stderr).write
246+
sys.stdout = _LoggingHandler("INFO", sys.stderr)
247+
sys.stderr = _LoggingHandler("ERROR", sys.stderr)
248+
224249
# Extract the target function from the source file
225250
if not hasattr(source_module, target):
226251
raise MissingTargetException(

tests/test_functions.py

+25
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import os
1717
import pathlib
1818
import re
19+
import sys
1920
import time
2021

2122
import pretend
@@ -495,6 +496,30 @@ def test_legacy_function_check_env(monkeypatch):
495496
assert resp.data.decode("utf-8") == target
496497

497498

499+
@pytest.mark.parametrize(
500+
"mode, expected",
501+
[
502+
("loginfo", '"severity": "INFO"'),
503+
("logwarn", '"severity": "ERROR"'),
504+
("logerr", '"severity": "ERROR"'),
505+
("logcrit", '"severity": "ERROR"'),
506+
("stdout", '"severity": "INFO"'),
507+
("stderr", '"severity": "ERROR"'),
508+
],
509+
)
510+
def test_legacy_function_log_severity(monkeypatch, capfd, mode, expected):
511+
source = TEST_FUNCTIONS_DIR / "http_check_severity" / "main.py"
512+
target = "function"
513+
514+
monkeypatch.setenv("ENTRY_POINT", target)
515+
516+
client = create_app(target, source).test_client()
517+
resp = client.post("/", json={"mode": mode})
518+
captured = capfd.readouterr().err
519+
assert resp.status_code == 200
520+
assert expected in captured
521+
522+
498523
def test_legacy_function_returns_none(monkeypatch):
499524
source = TEST_FUNCTIONS_DIR / "returns_none" / "main.py"
500525
target = "function"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2020 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+
15+
"""Function used in Worker tests of legacy GCF Python 3.7 logging."""
16+
import logging
17+
import os
18+
import sys
19+
20+
X_GOOGLE_FUNCTION_NAME = "gcf-function"
21+
X_GOOGLE_ENTRY_POINT = "function"
22+
HOME = "/tmp"
23+
24+
25+
def function(request):
26+
"""Test function which logs to the appropriate output.
27+
28+
Args:
29+
request: The HTTP request which triggered this function. Must contain name
30+
of the requested output in the 'mode' field in JSON document
31+
in request body.
32+
33+
Returns:
34+
Value of the mode.
35+
"""
36+
name = request.get_json().get("mode")
37+
if name == "stdout":
38+
print("log")
39+
elif name == "stderr":
40+
print("log", file=sys.stderr)
41+
elif name == "loginfo":
42+
logging.info("log")
43+
elif name == "logwarn":
44+
logging.warning("log")
45+
elif name == "logerr":
46+
logging.error("log")
47+
elif name == "logcrit":
48+
logging.critical("log")
49+
return name

0 commit comments

Comments
 (0)