Skip to content

Commit cce9c99

Browse files
liyanhui1228Jon Wayne Parrott
authored andcommitted
Stop writing to '/var/log/app_engine/' and write logs to Stackdriver logging API (#3410)
1 parent f6dab41 commit cce9c99

File tree

12 files changed

+144
-102
lines changed

12 files changed

+144
-102
lines changed

logging/google/cloud/logging/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def get_default_handler(self):
303303
"""
304304
if (_APPENGINE_FLEXIBLE_ENV_VM in os.environ or
305305
_APPENGINE_FLEXIBLE_ENV_FLEX in os.environ):
306-
return AppEngineHandler()
306+
return AppEngineHandler(self)
307307
elif _CONTAINER_ENGINE_ENV in os.environ:
308308
return ContainerEngineHandler()
309309
else:

logging/google/cloud/logging/handlers/app_engine.py

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,60 +14,56 @@
1414

1515
"""Logging handler for App Engine Flexible
1616
17-
Logs to the well-known file that the fluentd sidecar container on App Engine
18-
Flexible is configured to read from and send to Stackdriver Logging.
19-
20-
See the fluentd configuration here:
21-
22-
https://github.com/GoogleCloudPlatform/appengine-sidecars-docker/tree/master/fluentd_logger
17+
Sends logs to the Stackdriver Logging API with the appropriate resource
18+
and labels for App Engine logs.
2319
"""
2420

25-
# This file is largely copied from:
26-
# https://github.com/GoogleCloudPlatform/python-compat-runtime/blob/master
27-
# /appengine-vmruntime/vmruntime/cloud_logging.py
28-
29-
import logging.handlers
3021
import os
3122

32-
from google.cloud.logging.handlers._helpers import format_stackdriver_json
23+
from google.cloud.logging.handlers.handlers import CloudLoggingHandler
24+
from google.cloud.logging.handlers.transports import BackgroundThreadTransport
25+
from google.cloud.logging.resource import Resource
3326

34-
_LOG_PATH_TEMPLATE = '/var/log/app_engine/app.{pid}.json'
35-
_MAX_LOG_BYTES = 128 * 1024 * 1024
36-
_LOG_FILE_COUNT = 3
27+
_DEFAULT_GAE_LOGGER_NAME = 'app'
3728

29+
_GAE_PROJECT_ENV = 'GCLOUD_PROJECT'
30+
_GAE_SERVICE_ENV = 'GAE_SERVICE'
31+
_GAE_VERSION_ENV = 'GAE_VERSION'
3832

39-
class AppEngineHandler(logging.handlers.RotatingFileHandler):
40-
"""A handler that writes to the App Engine fluentd Stackdriver log file.
4133

42-
Writes to the file that the fluentd agent on App Engine Flexible is
43-
configured to discover logs and send them to Stackdriver Logging.
44-
Log entries are wrapped in JSON and with appropriate metadata. The
45-
process of converting the user's formatted logs into a JSON payload for
46-
Stackdriver Logging consumption is implemented as part of the handler
47-
itself, and not as a formatting step, so as not to interfere with
48-
user-defined logging formats.
49-
"""
34+
class AppEngineHandler(CloudLoggingHandler):
35+
"""A logging handler that sends App Engine-formatted logs to Stackdriver.
5036
51-
def __init__(self):
52-
"""Construct the handler
37+
:type client: :class:`~google.cloud.logging.client.Client`
38+
:param client: The authenticated Google Cloud Logging client for this
39+
handler to use.
5340
54-
Large log entries will get mangled if multiple workers write to the
55-
same file simultaneously, so we'll use the worker's PID to pick a log
56-
filename.
57-
"""
58-
self.filename = _LOG_PATH_TEMPLATE.format(pid=os.getpid())
59-
super(AppEngineHandler, self).__init__(self.filename,
60-
maxBytes=_MAX_LOG_BYTES,
61-
backupCount=_LOG_FILE_COUNT)
41+
:type transport: type
42+
:param transport: The transport class. It should be a subclass
43+
of :class:`.Transport`. If unspecified,
44+
:class:`.BackgroundThreadTransport` will be used.
45+
"""
6246

63-
def format(self, record):
64-
"""Format the specified record into the expected JSON structure.
47+
def __init__(self, client,
48+
transport=BackgroundThreadTransport):
49+
super(AppEngineHandler, self).__init__(
50+
client,
51+
name=_DEFAULT_GAE_LOGGER_NAME,
52+
transport=transport,
53+
resource=self.get_gae_resource())
6554

66-
:type record: :class:`~logging.LogRecord`
67-
:param record: the log record
55+
def get_gae_resource(self):
56+
"""Return the GAE resource using the environment variables.
6857
69-
:rtype: str
70-
:returns: JSON str to be written to the log file
58+
:rtype: :class:`~google.cloud.logging.resource.Resource`
59+
:returns: Monitored resource for GAE.
7160
"""
72-
message = super(AppEngineHandler, self).format(record)
73-
return format_stackdriver_json(record, message)
61+
gae_resource = Resource(
62+
type='gae_app',
63+
labels={
64+
'project_id': os.environ.get(_GAE_PROJECT_ENV),
65+
'module_id': os.environ.get(_GAE_SERVICE_ENV),
66+
'version_id': os.environ.get(_GAE_VERSION_ENV),
67+
},
68+
)
69+
return gae_resource

logging/google/cloud/logging/handlers/handlers.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import logging
1818

1919
from google.cloud.logging.handlers.transports import BackgroundThreadTransport
20+
from google.cloud.logging.logger import _GLOBAL_RESOURCE
2021

2122
DEFAULT_LOGGER_NAME = 'python'
2223

@@ -52,6 +53,10 @@ class CloudLoggingHandler(logging.StreamHandler):
5253
:class:`.BackgroundThreadTransport`. The other
5354
option is :class:`.SyncTransport`.
5455
56+
:type resource: :class:`~google.cloud.logging.resource.Resource`
57+
:param resource: (Optional) Monitored resource of the entry, defaults
58+
to the global resource type.
59+
5560
Example:
5661
5762
.. code-block:: python
@@ -73,11 +78,13 @@ class CloudLoggingHandler(logging.StreamHandler):
7378

7479
def __init__(self, client,
7580
name=DEFAULT_LOGGER_NAME,
76-
transport=BackgroundThreadTransport):
81+
transport=BackgroundThreadTransport,
82+
resource=_GLOBAL_RESOURCE):
7783
super(CloudLoggingHandler, self).__init__()
7884
self.name = name
7985
self.client = client
8086
self.transport = transport(client, name)
87+
self.resource = resource
8188

8289
def emit(self, record):
8390
"""Actually log the specified logging record.
@@ -90,7 +97,7 @@ def emit(self, record):
9097
:param record: The record to be logged.
9198
"""
9299
message = super(CloudLoggingHandler, self).format(record)
93-
self.transport.send(record, message)
100+
self.transport.send(record, message, resource=self.resource)
94101

95102

96103
def setup_logging(handler, excluded_loggers=EXCLUDED_LOGGER_DEFAULTS,

logging/google/cloud/logging/handlers/transports/background_thread.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def _main_thread_terminated(self):
203203
else:
204204
print('Failed to send %d pending logs.' % (self._queue.qsize(),))
205205

206-
def enqueue(self, record, message):
206+
def enqueue(self, record, message, resource=None):
207207
"""Queues a log entry to be written by the background thread.
208208
209209
:type record: :class:`logging.LogRecord`
@@ -212,13 +212,17 @@ def enqueue(self, record, message):
212212
:type message: str
213213
:param message: The message from the ``LogRecord`` after being
214214
formatted by the associated log formatters.
215+
216+
:type resource: :class:`~google.cloud.logging.resource.Resource`
217+
:param resource: (Optional) Monitored resource of the entry
215218
"""
216219
self._queue.put_nowait({
217220
'info': {
218221
'message': message,
219222
'python_logger': record.name,
220223
},
221224
'severity': record.levelname,
225+
'resource': resource,
222226
})
223227

224228
def flush(self):
@@ -253,7 +257,7 @@ def __init__(self, client, name, grace_period=_DEFAULT_GRACE_PERIOD,
253257
self.worker = _Worker(logger)
254258
self.worker.start()
255259

256-
def send(self, record, message):
260+
def send(self, record, message, resource=None):
257261
"""Overrides Transport.send().
258262
259263
:type record: :class:`logging.LogRecord`
@@ -262,8 +266,11 @@ def send(self, record, message):
262266
:type message: str
263267
:param message: The message from the ``LogRecord`` after being
264268
formatted by the associated log formatters.
269+
270+
:type resource: :class:`~google.cloud.logging.resource.Resource`
271+
:param resource: (Optional) Monitored resource of the entry.
265272
"""
266-
self.worker.enqueue(record, message)
273+
self.worker.enqueue(record, message, resource=resource)
267274

268275
def flush(self):
269276
"""Submit any pending log records."""

logging/google/cloud/logging/handlers/transports/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Transport(object):
2222
client and name object, and must override :meth:`send`.
2323
"""
2424

25-
def send(self, record, message):
25+
def send(self, record, message, resource=None):
2626
"""Transport send to be implemented by subclasses.
2727
2828
:type record: :class:`logging.LogRecord`
@@ -31,6 +31,9 @@ def send(self, record, message):
3131
:type message: str
3232
:param message: The message from the ``LogRecord`` after being
3333
formatted by the associated log formatters.
34+
35+
:type resource: :class:`~google.cloud.logging.resource.Resource`
36+
:param resource: (Optional) Monitored resource of the entry.
3437
"""
3538
raise NotImplementedError
3639

logging/google/cloud/logging/handlers/transports/sync.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class SyncTransport(Transport):
2929
def __init__(self, client, name):
3030
self.logger = client.logger(name)
3131

32-
def send(self, record, message):
32+
def send(self, record, message, resource=None):
3333
"""Overrides transport.send().
3434
3535
:type record: :class:`logging.LogRecord`
@@ -40,4 +40,6 @@ def send(self, record, message):
4040
formatted by the associated log formatters.
4141
"""
4242
info = {'message': message, 'python_logger': record.name}
43-
self.logger.log_struct(info, severity=record.levelname)
43+
self.logger.log_struct(info,
44+
severity=record.levelname,
45+
resource=resource)

logging/tests/unit/handlers/test_app_engine.py

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import logging
1516
import unittest
1617

1718

@@ -24,34 +25,47 @@ def _get_target_class(self):
2425
return AppEngineHandler
2526

2627
def _make_one(self, *args, **kw):
27-
import tempfile
28+
return self._get_target_class()(*args, **kw)
2829

29-
from google.cloud._testing import _Monkey
30-
from google.cloud.logging.handlers import app_engine as _MUT
30+
def test_constructor(self):
31+
import mock
32+
from google.cloud.logging.handlers.app_engine import _GAE_PROJECT_ENV
33+
from google.cloud.logging.handlers.app_engine import _GAE_SERVICE_ENV
34+
from google.cloud.logging.handlers.app_engine import _GAE_VERSION_ENV
3135

32-
tmpdir = tempfile.mktemp()
33-
with _Monkey(_MUT, _LOG_PATH_TEMPLATE=tmpdir):
34-
return self._get_target_class()(*args, **kw)
36+
client = mock.Mock(project=self.PROJECT, spec=['project'])
37+
with mock.patch('os.environ', new={_GAE_PROJECT_ENV: 'test_project',
38+
_GAE_SERVICE_ENV: 'test_service',
39+
_GAE_VERSION_ENV: 'test_version'}):
40+
handler = self._make_one(client, transport=_Transport)
41+
self.assertIs(handler.client, client)
42+
self.assertEqual(handler.resource.type, 'gae_app')
43+
self.assertEqual(handler.resource.labels['project_id'], 'test_project')
44+
self.assertEqual(handler.resource.labels['module_id'], 'test_service')
45+
self.assertEqual(handler.resource.labels['version_id'], 'test_version')
3546

36-
def test_format(self):
37-
import json
38-
import logging
47+
def test_emit(self):
48+
import mock
3949

40-
handler = self._make_one()
41-
logname = 'loggername'
50+
client = mock.Mock(project=self.PROJECT, spec=['project'])
51+
handler = self._make_one(client, transport=_Transport)
52+
gae_resource = handler.get_gae_resource()
53+
logname = 'app'
4254
message = 'hello world'
43-
record = logging.LogRecord(logname, logging.INFO, None,
44-
None, message, None, None)
45-
record.created = 5.03
46-
expected_payload = {
47-
'message': message,
48-
'timestamp': {
49-
'seconds': 5,
50-
'nanos': int(.03 * 1e9),
51-
},
52-
'thread': record.thread,
53-
'severity': record.levelname,
54-
}
55-
payload = handler.format(record)
56-
57-
self.assertEqual(payload, json.dumps(expected_payload))
55+
record = logging.LogRecord(logname, logging, None, None, message,
56+
None, None)
57+
handler.emit(record)
58+
59+
self.assertIs(handler.transport.client, client)
60+
self.assertEqual(handler.transport.name, logname)
61+
self.assertEqual(handler.transport.send_called_with, (record, message, gae_resource))
62+
63+
64+
class _Transport(object):
65+
66+
def __init__(self, client, name):
67+
self.client = client
68+
self.name = name
69+
70+
def send(self, record, message, resource):
71+
self.send_called_with = (record, message, resource)

logging/tests/unit/handlers/test_handlers.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,17 @@ def test_ctor(self):
3535
self.assertEqual(handler.client, client)
3636

3737
def test_emit(self):
38+
from google.cloud.logging.logger import _GLOBAL_RESOURCE
39+
3840
client = _Client(self.PROJECT)
39-
handler = self._make_one(client, transport=_Transport)
41+
handler = self._make_one(client, transport=_Transport, resource=_GLOBAL_RESOURCE)
4042
logname = 'loggername'
4143
message = 'hello world'
4244
record = logging.LogRecord(logname, logging, None, None, message,
4345
None, None)
4446
handler.emit(record)
4547

46-
self.assertEqual(handler.transport.send_called_with, (record, message))
48+
self.assertEqual(handler.transport.send_called_with, (record, message, _GLOBAL_RESOURCE))
4749

4850

4951
class TestSetupLogging(unittest.TestCase):
@@ -108,5 +110,5 @@ class _Transport(object):
108110
def __init__(self, client, name):
109111
pass
110112

111-
def send(self, record, message):
112-
self.send_called_with = (record, message)
113+
def send(self, record, message, resource):
114+
self.send_called_with = (record, message, resource)

logging/tests/unit/handlers/transports/test_background_thread.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,23 @@ def test_constructor(self):
4747
self.assertEqual(logger.name, name)
4848

4949
def test_send(self):
50+
from google.cloud.logging.logger import _GLOBAL_RESOURCE
51+
5052
client = _Client(self.PROJECT)
5153
name = 'python_logger'
5254

5355
transport, _ = self._make_one(client, name)
5456

5557
python_logger_name = 'mylogger'
5658
message = 'hello world'
59+
5760
record = logging.LogRecord(
5861
python_logger_name, logging.INFO,
5962
None, None, message, None, None)
6063

61-
transport.send(record, message)
64+
transport.send(record, message, _GLOBAL_RESOURCE)
6265

63-
transport.worker.enqueue.assert_called_once_with(record, message)
66+
transport.worker.enqueue.assert_called_once_with(record, message, _GLOBAL_RESOURCE)
6467

6568
def test_flush(self):
6669
client = _Client(self.PROJECT)
@@ -284,8 +287,13 @@ def __init__(self):
284287
self.commit_called = False
285288
self.commit_count = None
286289

287-
def log_struct(self, info, severity=logging.INFO):
288-
self.log_struct_called_with = (info, severity)
290+
def log_struct(self, info, severity=logging.INFO, resource=None):
291+
from google.cloud.logging.logger import _GLOBAL_RESOURCE
292+
293+
if resource is None:
294+
resource = _GLOBAL_RESOURCE
295+
296+
self.log_struct_called_with = (info, severity, resource)
289297
self.entries.append(info)
290298

291299
def commit(self):

logging/tests/unit/handlers/transports/test_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def _make_one(self, *args, **kw):
3131
def test_send_is_abstract(self):
3232
target = self._make_one()
3333
with self.assertRaises(NotImplementedError):
34-
target.send(None, None)
34+
target.send(None, None, None)
3535

3636
def test_flush_is_abstract_and_optional(self):
3737
target = self._make_one()

0 commit comments

Comments
 (0)