Skip to content

Commit 1f5982f

Browse files
fix: structured log handler formatting issues (#319)
1 parent 9c97529 commit 1f5982f

File tree

20 files changed

+654
-47
lines changed

20 files changed

+654
-47
lines changed

packages/google-cloud-logging/google/cloud/logging_v2/handlers/structured_log.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414

1515
"""Logging handler for printing formatted structured logs to standard output.
1616
"""
17-
17+
import json
1818
import logging.handlers
1919

2020
from google.cloud.logging_v2.handlers.handlers import CloudLoggingFilter
2121

2222
GCP_FORMAT = (
23-
'{"message": "%(_msg_str)s", '
23+
'{"message": %(_formatted_msg)s, '
2424
'"severity": "%(levelname)s", '
2525
'"logging.googleapis.com/labels": %(_labels_str)s, '
2626
'"logging.googleapis.com/trace": "%(_trace_str)s", '
@@ -50,7 +50,7 @@ def __init__(self, *, labels=None, stream=None, project_id=None):
5050
self.addFilter(log_filter)
5151

5252
# make logs appear in GCP structured logging format
53-
self.formatter = logging.Formatter(GCP_FORMAT)
53+
self._gcp_formatter = logging.Formatter(GCP_FORMAT)
5454

5555
def format(self, record):
5656
"""Format the message into structured log JSON.
@@ -59,6 +59,12 @@ def format(self, record):
5959
Returns:
6060
str: A JSON string formatted for GKE fluentd.
6161
"""
62-
63-
payload = self.formatter.format(record)
64-
return payload
62+
# let other formatters alter the message
63+
super_payload = None
64+
if record.msg:
65+
super_payload = super(StructuredLogHandler, self).format(record)
66+
# properly break any formatting in string to make it json safe
67+
record._formatted_msg = json.dumps(super_payload or "")
68+
# convert to GCP structred logging format
69+
gcp_payload = self._gcp_formatter.format(record)
70+
return gcp_payload

packages/google-cloud-logging/tests/environment/deployable/nodejs/Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ FROM node:12-slim
1919
# Create and change to the app directory.
2020
WORKDIR /usr/src/app
2121

22-
# Copy test script and dependencies to the container image.
22+
# Copy test script and local dependencies to the container image.
2323
COPY package*.json ./
2424
COPY app.js ./
2525
COPY tests.js ./
26+
# Assumption: local file is already built
2627
COPY nodejs-logging ./nodejs-logging
2728

28-
# Install dependencies.
29+
# Install test app's dependencies.
2930
RUN npm install --production
3031

3132
# Environment variable denoting whether to run an app server

packages/google-cloud-logging/tests/environment/deployable/nodejs/app.js

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,30 @@
1414

1515
var tests = require('./tests.js');
1616

17+
/** ****************** GAE, GKE, GCE ******************
18+
* Enable app subscriber for all environments, except GCR, GCF.
19+
*/
20+
async function enableSubscriber() {
21+
if (process.env.ENABLE_SUBSCRIBER) {
22+
const gcpMetadata = require('gcp-metadata');
23+
const projectId = await gcpMetadata.project('project-id');
24+
const topicId = process.env.PUBSUB_TOPIC || 'logging-test';
25+
const subscriptionId = `${topicId}-subscriber`;
26+
const topicName = `projects/${projectId}/topics/${topicId}`;
27+
const subscriptionName = `projects/${projectId}/subscriptions/${subscriptionId}`
28+
29+
const {PubSub} = require('@google-cloud/pubsub');
30+
const pubSubClient = new PubSub();
31+
// Creates a new subscription
32+
pubSubClient.topic(topicName).createSubscription(subscriptionName);
33+
listenForMessages(pubSubClient, subscriptionName).catch(console.error);
34+
}
35+
}
36+
enableSubscriber().catch(console.error);
37+
1738
/**
18-
* Only triggers for GCP services that require a running app server.
19-
* For instance, Cloud Functions does not execute this block.
39+
* ****************** GCR, GKE, GCE ******************
40+
* For GCP services that require a running app server, except GAE and GCF.
2041
* RUNSERVER env var is set in the Dockerfile.
2142
*/
2243
if (process.env.RUNSERVER) {
@@ -29,25 +50,27 @@ if (process.env.RUNSERVER) {
2950
/**
3051
* Cloud Run to be triggered by Pub/Sub.
3152
*/
32-
app.post('/', (req, res) => {
33-
if (!req.body) {
34-
const msg = 'no Pub/Sub message received';
35-
console.error(`error: ${msg}`);
36-
res.status(400).send(`Bad Request: ${msg}`);
37-
return;
38-
}
39-
if (!req.body.message) {
40-
const msg = 'invalid Pub/Sub message format';
41-
console.error(`error: ${msg}`);
42-
res.status(400).send(`Bad Request: ${msg}`);
43-
return;
44-
}
45-
46-
const message = req.body.message;
47-
triggerTest(message);
53+
if (process.env.K_CONFIGURATION) {
54+
app.post('/', (req, res) => {
55+
if (!req.body) {
56+
const msg = 'no Pub/Sub message received';
57+
console.error(`error: ${msg}`);
58+
res.status(400).send(`Bad Request: ${msg}`);
59+
return;
60+
}
61+
if (!req.body.message) {
62+
const msg = 'invalid Pub/Sub message format';
63+
console.error(`error: ${msg}`);
64+
res.status(400).send(`Bad Request: ${msg}`);
65+
return;
66+
}
4867

49-
res.status(204).send();
50-
});
68+
const message = req.body.message;
69+
triggerTest(message);
70+
71+
res.status(204).send();
72+
});
73+
};
5174

5275
// Start app server
5376
const PORT = process.env.PORT || 8080;
@@ -68,6 +91,31 @@ exports.pubsubFunction = (message, context) => {
6891
triggerTest(message);
6992
};
7093

94+
/**
95+
* ****************** GAE, GKE, GCE ******************
96+
* Asynchronously listens for pubsub messages until a TIMEOUT is reached.
97+
* @param pubSubClient
98+
* @param subscriptionName
99+
*/
100+
async function listenForMessages(pubSubClient, subscriptionName) {
101+
// References an existing subscription
102+
const subscription = pubSubClient.subscription(subscriptionName);
103+
104+
// Handles incoming messages and triggers tests.
105+
const messageHandler = message => {
106+
triggerTest(message);
107+
// "Ack" (acknowledge receipt of) the message
108+
message.ack();
109+
};
110+
111+
// Listen for new messages until timeout is hit or test is done.
112+
subscription.on('message', messageHandler);
113+
114+
setTimeout(() => {
115+
subscription.removeListener('message', messageHandler);
116+
}, 600000); // max 10 minutes timeout
117+
}
118+
71119
function triggerTest(message) {
72120
const testName = message.data
73121
? Buffer.from(message.data, 'base64').toString()

packages/google-cloud-logging/tests/environment/deployable/nodejs/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
"description": "",
66
"main": "app.js",
77
"dependencies": {
8+
"@google-cloud/logging": "file:nodejs-logging",
9+
"@google-cloud/pubsub": "^2.12.0",
810
"body-parser": "^1.19.0",
911
"express": "^4.17.1",
10-
"@google-cloud/logging": "file:nodejs-logging"
12+
"gcp-metadata": "^4.2.1"
1113
},
1214
"devDependencies": {},
1315
"engines": {

packages/google-cloud-logging/tests/environment/deployable/python/snippets.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,21 @@ def pylogging(log_text="pylogging", severity="WARNING", **kwargs):
100100
else:
101101
logging.critical(log_text, extra=kwargs)
102102

103+
def pylogging_multiline(log_text="pylogging", second_line="line 2", **kwargs):
104+
logging.error(f"{log_text}\n{second_line}")
105+
106+
def pylogging_complex_chars(**kwargs):
107+
logging.error('}"{!@[')
108+
109+
def pylogging_with_formatter(log_text="pylogging", format_str="%(name)s :: %(levelname)s :: %(message)s", **kwargs):
110+
root_logger = logging.getLogger()
111+
handler = root_logger.handlers[0]
112+
handler.setFormatter(logging.Formatter(fmt=format_str))
113+
logging.error(log_text)
114+
handler.setFormatter(None)
115+
116+
def pylogging_with_arg(log_text="my_arg", **kwargs):
117+
logging.error("Arg: %s", log_text)
103118

104119
def pylogging_flask(
105120
log_text="pylogging_flask",

packages/google-cloud-logging/tests/environment/envctl/env_scripts/go/kubernetes.sh

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ build_go_container(){
8282
}
8383

8484
deploy() {
85-
# local SCRIPT="${1:-router.py}"
86-
# TODO: double check this doesn't print impt otkens/pws
87-
set -x
8885
attach_or_create_gke_cluster
8986
build_go_container
9087
cat <<EOF > $TMP_DIR/gke.yaml
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/bin/bash
2+
# Copyright 2021 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
set -e # exit on any failure
17+
set -o pipefail # any step in pipe caused failure
18+
set -u # undefined variables cause exit
19+
20+
SERVICE_NAME="log-node-gae-$(echo $ENVCTL_ID | head -c 8)"
21+
LIBRARY_NAME="nodejs-logging"
22+
23+
destroy() {
24+
set +e
25+
# delete pubsub resources
26+
gcloud pubsub topics delete $SERVICE_NAME -q 2> /dev/null
27+
gcloud pubsub subscriptions delete $SERVICE_NAME-subscriber -q 2> /dev/null
28+
# delete service
29+
gcloud app services delete $SERVICE_NAME -q 2> /dev/null
30+
set -e
31+
}
32+
33+
verify() {
34+
set +e
35+
gcloud app services describe $SERVICE_NAME -q > /dev/null 2> /dev/null
36+
if [[ $? == 0 ]]; then
37+
echo "TRUE"
38+
exit 0
39+
else
40+
echo "FALSE"
41+
exit 1
42+
fi
43+
set -e
44+
}
45+
46+
deploy() {
47+
# create pub/sub topic
48+
set +e
49+
gcloud pubsub topics create $SERVICE_NAME 2>/dev/null
50+
set -ex
51+
52+
# copy over local version of the library
53+
pushd $SUPERREPO_ROOT
54+
tar -cvf $TMP_DIR/lib.tar --exclude node_modules --exclude env-tests-logging --exclude test --exclude system-test --exclude .nox --exclude samples --exclude docs .
55+
popd
56+
mkdir -p $TMP_DIR/$LIBRARY_NAME
57+
tar -xvf $TMP_DIR/lib.tar --directory $TMP_DIR/$LIBRARY_NAME
58+
59+
# Copy over test code and Node dependencies
60+
cp $REPO_ROOT/deployable/nodejs/package.json $TMP_DIR
61+
cp $REPO_ROOT/deployable/nodejs/*.js $TMP_DIR
62+
63+
# manual_scaling allows 1 instance to continuously run regardless of the load level.
64+
cat <<EOF > $TMP_DIR/app.yaml
65+
runtime: nodejs12
66+
service: $SERVICE_NAME
67+
manual_scaling:
68+
instances: 1
69+
env_variables:
70+
ENABLE_SUBSCRIBER: "true"
71+
PUBSUB_TOPIC: $SERVICE_NAME
72+
EOF
73+
# deploy
74+
pushd $TMP_DIR
75+
gcloud app deploy -q
76+
popd
77+
# wait for the pub/sub subscriber to start
78+
NUM_SUBSCRIBERS=0
79+
TRIES=0
80+
while [[ "${NUM_SUBSCRIBERS}" -lt 1 && "${TRIES}" -lt 10 ]]; do
81+
sleep 30
82+
NUM_SUBSCRIBERS=$(gcloud pubsub topics list-subscriptions $SERVICE_NAME 2> /dev/null | wc -l)
83+
TRIES=$((TRIES + 1))
84+
done
85+
}
86+
87+
filter-string() {
88+
echo "resource.type=\"gae_app\" AND resource.labels.module_id=\"$SERVICE_NAME\""
89+
}

packages/google-cloud-logging/tests/environment/envctl/env_scripts/nodejs/cloudrun.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ build_node_container() {
8787
}
8888

8989
deploy() {
90-
set -x
9190
build_node_container
9291
gcloud config set run/platform managed
9392
gcloud config set run/region us-west1

0 commit comments

Comments
 (0)