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 backwards-compatible logging for GCF Python 3.7 #107

Merged
merged 9 commits into from
Feb 17, 2021
16 changes: 16 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
import sys

from importlib import reload

import pytest

Expand All @@ -26,3 +30,15 @@ def isolate_environment():
finally:
os.environ.clear()
os.environ.update(_environ)


@pytest.fixture(scope="function", autouse=True)
def isolate_logging():
"Ensure any changes to logging are isolated to individual tests" ""
try:
yield
finally:
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
logging.shutdown()
reload(logging)
25 changes: 25 additions & 0 deletions src/functions_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import functools
import importlib.util
import io
import json
import os.path
import pathlib
Expand Down Expand Up @@ -65,6 +66,19 @@ def __init__(
self.data = data


class _LoggingHandler(io.TextIOWrapper):
"""Logging replacement for stdout and stderr in GCF Python 3.7."""

def __init__(self, level, stderr=sys.stderr):
io.TextIOWrapper.__init__(self, io.StringIO(), encoding=stderr.encoding)
self.level = level
self.stderr = stderr

def write(self, out):
payload = dict(severity=self.level, message=out.rstrip("\n"))
return self.stderr.write(json.dumps(payload) + "\n")


def _http_view_func_wrapper(function, request):
def view_func(path):
return function(request._get_current_object())
Expand Down Expand Up @@ -221,6 +235,17 @@ def handle_none(rv):

app.make_response = handle_none

# Handle log severity backwards compatibility
import logging # isort:skip

logging.info = _LoggingHandler("INFO", sys.stderr).write
logging.warn = _LoggingHandler("ERROR", sys.stderr).write
logging.warning = _LoggingHandler("ERROR", sys.stderr).write
logging.error = _LoggingHandler("ERROR", sys.stderr).write
logging.critical = _LoggingHandler("ERROR", sys.stderr).write
sys.stdout = _LoggingHandler("INFO", sys.stderr)
sys.stderr = _LoggingHandler("ERROR", sys.stderr)
di marked this conversation as resolved.
Show resolved Hide resolved

# Extract the target function from the source file
if not hasattr(source_module, target):
raise MissingTargetException(
Expand Down
25 changes: 25 additions & 0 deletions tests/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import os
import pathlib
import re
import sys
import time

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


@pytest.mark.parametrize(
"mode, expected",
[
("loginfo", '"severity": "INFO"'),
("logwarn", '"severity": "ERROR"'),
("logerr", '"severity": "ERROR"'),
("logcrit", '"severity": "ERROR"'),
("stdout", '"severity": "INFO"'),
("stderr", '"severity": "ERROR"'),
],
)
def test_legacy_function_log_severity(monkeypatch, capfd, mode, expected):
source = TEST_FUNCTIONS_DIR / "http_check_severity" / "main.py"
target = "function"

monkeypatch.setenv("ENTRY_POINT", target)

client = create_app(target, source).test_client()
resp = client.post("/", json={"mode": mode})
captured = capfd.readouterr().err
assert resp.status_code == 200
assert expected in captured


def test_legacy_function_returns_none(monkeypatch):
source = TEST_FUNCTIONS_DIR / "returns_none" / "main.py"
target = "function"
Expand Down
49 changes: 49 additions & 0 deletions tests/test_functions/http_check_severity/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Function used in Worker tests of legacy GCF Python 3.7 logging."""
import logging
import os
import sys

X_GOOGLE_FUNCTION_NAME = "gcf-function"
X_GOOGLE_ENTRY_POINT = "function"
HOME = "/tmp"


def function(request):
"""Test function which logs to the appropriate output.

Args:
request: The HTTP request which triggered this function. Must contain name
of the requested output in the 'mode' field in JSON document
in request body.

Returns:
Value of the mode.
"""
name = request.get_json().get("mode")
if name == "stdout":
print("log")
elif name == "stderr":
print("log", file=sys.stderr)
elif name == "loginfo":
logging.info("log")
elif name == "logwarn":
logging.warning("log")
elif name == "logerr":
logging.error("log")
elif name == "logcrit":
logging.critical("log")
return name