diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3ae4d9d..c4ec7480 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [2.1.1] - 2021-02-17
+### Fixed
+- Add backwards-compatible logging for GCF Python 3.7 ([#107])
+- Document `--dry-run` flag ([#105])
+
## [2.1.0] - 2020-12-23
### Added
- Support Python 3.9
@@ -85,7 +90,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Initial release
-[Unreleased]: https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v2.1.0...HEAD
+[Unreleased]: https://github.com/GoogleCloudPlatform/functions-framework-python/compare/v2.1.1...HEAD
+[2.1.1]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v2.1.1
[2.1.0]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v2.1.0
[2.0.0]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v2.0.0
[1.6.0]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v1.6.0
@@ -102,6 +108,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[1.0.1]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v1.0.1
[1.0.0]: https://github.com/GoogleCloudPlatform/functions-framework-python/releases/tag/v1.0.0
+[#107]: https://github.com/GoogleCloudPlatform/functions-framework-python/pull/107
+[#105]: https://github.com/GoogleCloudPlatform/functions-framework-python/pull/105
[#77]: https://github.com/GoogleCloudPlatform/functions-framework-python/pull/77
[#76]: https://github.com/GoogleCloudPlatform/functions-framework-python/pull/76
[#70]: https://github.com/GoogleCloudPlatform/functions-framework-python/pull/70
diff --git a/README.md b/README.md
index 0d8d5a7e..312aff46 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ pip install functions-framework
Or, for deployment, add the Functions Framework to your `requirements.txt` file:
```
-functions-framework==2.1.0
+functions-framework==2.1.1
```
## Quickstarts
@@ -91,7 +91,7 @@ functions-framework --target hello --debug
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
- * Debug mode: off
+ * Debug mode: on
* Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
```
@@ -182,6 +182,7 @@ You can configure the Functions Framework using command-line flags or environmen
| `--signature-type` | `FUNCTION_SIGNATURE_TYPE` | The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function. Default: `http`; accepted values: `http`, `event` or `cloudevent` |
| `--source` | `FUNCTION_SOURCE` | The path to the file containing your function. Default: `main.py` (in the current working directory) |
| `--debug` | `DEBUG` | A flag that allows to run functions-framework to run in debug mode, including live reloading. Default: `False` |
+| `--dry-run` | `DRY_RUN` | A flag that allows for testing the function build from the configuration without creating a server. Default: `False` |
## Enable Google Cloud Functions Events
diff --git a/conftest.py b/conftest.py
index b8d44d43..21572fda 100644
--- a/conftest.py
+++ b/conftest.py
@@ -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
@@ -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)
diff --git a/examples/skaffold/README.md b/examples/skaffold/README.md
new file mode 100644
index 00000000..d115e30a
--- /dev/null
+++ b/examples/skaffold/README.md
@@ -0,0 +1,139 @@
+# Developing multiple functions on the same host using Minikube and Skaffold
+
+## Introduction
+
+This example shows you how to develop multiple Cloud Functions to a single host
+using Minikube and Skaffold.
+
+The example will focus on:
+* taking two separate Cloud Functions (defined in the same file)
+* building them each individually with Cloud Buildpacks and the Functions Framework
+* deploying them to a local Kubernetes cluster with `minikube` and `skaffold`
+* including live reloading!
+
+## Install `minikube`
+*Note: If on Cloud Shell, `minikube` is pre-installed.*
+
+Install `minikube` via the instructions for your platform at
+
+Confirm that `minikube` is installed:
+
+```bash
+minikube version
+```
+
+You should see output similar to:
+
+```terminal
+minikube version: v1.15.1
+commit: 23f40a012abb52eff365ff99a709501a61ac5876
+```
+
+## Start `minikube`
+
+This starts `minikube` using the default profile:
+
+```bash
+minikube start
+```
+
+This may take a few minutes.
+
+*Note: If on Cloud Shell, you may be asked to enable Cloud Shell to make API calls*
+
+You should see output similar to:
+
+```terminal
+😄 minikube v1.15.1 on Debian 10.6
+ ▪ MINIKUBE_FORCE_SYSTEMD=true
+ ▪ MINIKUBE_HOME=/google/minikube
+ ▪ MINIKUBE_WANTUPDATENOTIFICATION=false
+✨ Automatically selected the docker driver
+👍 Starting control plane node minikube in cluster minikube
+🚜 Pulling base image ...
+💾 Downloading Kubernetes v1.19.4 preload ...
+🔥 Creating docker container (CPUs=2, Memory=4000MB) ...
+🐳 Preparing Kubernetes v1.19.4 on Docker 19.03.13 ...
+🔎 Verifying Kubernetes components...
+🌟 Enabled addons: storage-provisioner, default-storageclass
+🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
+```
+
+## Install the `ingress` addon for `minikube`
+
+This allows `minikube` to handle external traffic:
+
+```bash
+minikube addons enable ingress
+```
+
+You should see output similar to:
+
+```terminal
+🔎 Verifying ingress addon...
+🌟 The 'ingress' addon is enabled
+```
+
+## Install `skaffold`
+*Note: If on Cloud Shell, `skaffold` is pre-installed.*
+
+Install `skaffold` via the instructions for your platform at
+
+Confirm that `skaffold` is installed:
+
+```bash
+skaffold version
+```
+
+You should see output similar to:
+
+```terminal
+v1.16.0
+```
+
+## Start `skaffold`
+
+Start `skaffold` with:
+
+```bash
+skaffold dev
+```
+
+You should see output similar to:
+
+```terminal
+Starting deploy...
+Waiting for deployments to stabilize...
+ - deployment/hello is ready. [1/2 deployment(s) still pending]
+ - deployment/goodbye is ready.
+Deployments stabilized in 1.154162006s
+Watching for changes...
+```
+
+This command will continue running indefinitely, watching for changes and redeploying as necessary.
+
+## Call your Cloud Functions
+
+Leaving the previous command running, in a **new terminal**, call your functions. To call the `hello` function:
+
+```bash
+curl `minikube ip`/hello
+```
+
+You should see output similar to:
+
+```terminal
+Hello, World!
+```
+
+To call the `goodbye` function:
+
+```bash
+curl `minikube ip`/goodbye
+```
+
+You should see output similar to:
+
+```terminal
+Goodbye, World!
+```
diff --git a/examples/skaffold/k8s/goodbye.yaml b/examples/skaffold/k8s/goodbye.yaml
new file mode 100644
index 00000000..6a891230
--- /dev/null
+++ b/examples/skaffold/k8s/goodbye.yaml
@@ -0,0 +1,47 @@
+# 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.
+
+apiVersion: v1
+kind: Service
+metadata:
+ name: goodbye
+spec:
+ ports:
+ - port: 8080
+ name: http
+ type: LoadBalancer
+ selector:
+ app: goodbye
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: goodbye
+spec:
+ selector:
+ matchLabels:
+ app: goodbye
+ template:
+ metadata:
+ labels:
+ app: goodbye
+ spec:
+ containers:
+ - name: goodbye
+ image: example-goodbye-image
+ env:
+ - name: PORT
+ value: "8080"
+ ports:
+ - containerPort: 8080
diff --git a/examples/skaffold/k8s/hello.yaml b/examples/skaffold/k8s/hello.yaml
new file mode 100644
index 00000000..68570ff6
--- /dev/null
+++ b/examples/skaffold/k8s/hello.yaml
@@ -0,0 +1,47 @@
+# 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.
+
+apiVersion: v1
+kind: Service
+metadata:
+ name: hello
+spec:
+ ports:
+ - port: 8080
+ name: http
+ type: LoadBalancer
+ selector:
+ app: hello
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: hello
+spec:
+ selector:
+ matchLabels:
+ app: hello
+ template:
+ metadata:
+ labels:
+ app: hello
+ spec:
+ containers:
+ - name: hello
+ image: example-hello-image
+ env:
+ - name: PORT
+ value: "8080"
+ ports:
+ - containerPort: 8080
diff --git a/examples/skaffold/k8s/ingress.yaml b/examples/skaffold/k8s/ingress.yaml
new file mode 100644
index 00000000..0abc2d6e
--- /dev/null
+++ b/examples/skaffold/k8s/ingress.yaml
@@ -0,0 +1,30 @@
+# 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.
+
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+ name: scaffold-example-ingress
+spec:
+ rules:
+ - http:
+ paths:
+ - path: /hello
+ backend:
+ serviceName: hello
+ servicePort: 8080
+ - path: /goodbye
+ backend:
+ serviceName: goodbye
+ servicePort: 8080
diff --git a/examples/skaffold/main.py b/examples/skaffold/main.py
new file mode 100644
index 00000000..298249be
--- /dev/null
+++ b/examples/skaffold/main.py
@@ -0,0 +1,23 @@
+# 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.
+
+
+def hello(request):
+ """Return a friendly HTTP greeting."""
+ return "Hello, World!"
+
+
+def goodbye(request):
+ """Return a friendly HTTP goodbye."""
+ return "Goodbye, World!"
diff --git a/examples/skaffold/requirements.txt b/examples/skaffold/requirements.txt
new file mode 100644
index 00000000..3601409f
--- /dev/null
+++ b/examples/skaffold/requirements.txt
@@ -0,0 +1 @@
+# Add any Python requirements here
diff --git a/examples/skaffold/skaffold.yaml b/examples/skaffold/skaffold.yaml
new file mode 100644
index 00000000..ca668226
--- /dev/null
+++ b/examples/skaffold/skaffold.yaml
@@ -0,0 +1,28 @@
+# 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.
+
+apiVersion: skaffold/v2beta9
+kind: Config
+build:
+ artifacts:
+ - image: example-hello-image
+ buildpacks:
+ builder: "gcr.io/buildpacks/builder:v1"
+ env:
+ - "GOOGLE_FUNCTION_TARGET=hello"
+ - image: example-goodbye-image
+ buildpacks:
+ builder: "gcr.io/buildpacks/builder:v1"
+ env:
+ - "GOOGLE_FUNCTION_TARGET=goodbye"
diff --git a/setup.py b/setup.py
index e03eed6f..63176dfa 100644
--- a/setup.py
+++ b/setup.py
@@ -25,7 +25,7 @@
setup(
name="functions-framework",
- version="2.1.0",
+ version="2.1.1",
description="An open source FaaS (Function as a service) framework for writing portable Python functions -- brought to you by the Google Cloud Functions team.",
long_description=long_description,
long_description_content_type="text/markdown",
diff --git a/src/functions_framework/__init__.py b/src/functions_framework/__init__.py
index 356f7a03..1e87fdb4 100644
--- a/src/functions_framework/__init__.py
+++ b/src/functions_framework/__init__.py
@@ -14,6 +14,7 @@
import functools
import importlib.util
+import io
import json
import os.path
import pathlib
@@ -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())
@@ -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)
+
# Extract the target function from the source file
if not hasattr(source_module, target):
raise MissingTargetException(
diff --git a/tests/test_functions.py b/tests/test_functions.py
index 2f0ca05b..5f746931 100644
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -16,6 +16,7 @@
import os
import pathlib
import re
+import sys
import time
import pretend
@@ -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"
diff --git a/tests/test_functions/http_check_severity/main.py b/tests/test_functions/http_check_severity/main.py
new file mode 100644
index 00000000..be586d8d
--- /dev/null
+++ b/tests/test_functions/http_check_severity/main.py
@@ -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