diff -r 27971a13089f -r 2e0b0af889be thirdparty/google_appengine/google/appengine/ext/ereporter/ereporter.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/thirdparty/google_appengine/google/appengine/ext/ereporter/ereporter.py Sun Sep 06 23:31:53 2009 +0200 @@ -0,0 +1,261 @@ +#!/usr/bin/env python +# +# Copyright 2007 Google Inc. +# +# 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. +# + +"""A logging handler that records information about unique exceptions. + +'Unique' in this case is defined as a given (exception class, location) tuple. +Unique exceptions are logged to the datastore with an example stacktrace and an +approximate count of occurrences, grouped by day and application version. + +A cron handler, in google.appengine.ext.ereporter.report_generator, constructs +and emails a report based on the previous day's exceptions. + +Example usage: + +In your handler script(s), add: + + import logging + from google.appengine.ext import ereporter + + ereporter.register_logger() + +In your app.yaml, add: + + handlers: + - url: /_ereporter/.* + script: $PYTHON_LIB/google/appengine/ext/ereporter/report_generator.py + login: admin + +In your cron.yaml, add: + + cron: + - description: Daily exception report + url: /_ereporter?sender=you@yourdomain.com + schedule: every day 00:00 + +This will cause a daily exception report to be generated and emailed to all +admins, with exception traces grouped by minor version. If you only want to +get exception information for the most recent minor version, add the +'versions=latest' argument to the query string. For other valid query string +arguments, see report_generator.py. + +If you anticipate a lot of exception traces (for example, if you're deploying +many minor versions, each of which may have its own set of exceptions), you +can ensure that the traces from the newest minor versions get included by adding +this to your index.yaml: + + indexes: + - kind: __google_ExceptionRecord + properties: + - name: date + - name: major_version + - name: minor_version + direction: desc +""" + + + + + +import datetime +import logging +import os +import sha +import traceback +import urllib + +from google.appengine.api import memcache +from google.appengine.ext import db +from google.appengine.ext import webapp + + +MAX_SIGNATURE_LENGTH = 256 + + +class ExceptionRecord(db.Model): + """Datastore model for a record of a unique exception.""" + + signature = db.StringProperty(required=True) + major_version = db.StringProperty(required=True) + minor_version = db.IntegerProperty(required=True) + date = db.DateProperty(required=True) + count = db.IntegerProperty(required=True, default=0) + + stacktrace = db.TextProperty(required=True) + http_method = db.TextProperty(required=True) + url = db.TextProperty(required=True) + handler = db.TextProperty(required=True) + + @classmethod + def get_key_name(cls, signature, version, date=None): + """Generates a key name for an exception record. + + Args: + signature: A signature representing the exception and its site. + version: The major/minor version of the app the exception occurred in. + date: The date the exception occurred. + + Returns: + The unique key name for this exception record. + """ + if not date: + date = datetime.date.today() + return '%s@%s:%s' % (signature, date, version) + + +class ExceptionRecordingHandler(logging.Handler): + """A handler that records exception data to the App Engine datastore.""" + + def __init__(self, log_interval=10): + """Constructs a new ExceptionRecordingHandler. + + Args: + log_interval: The minimum interval at which we will log an individual + exception. This is a per-exception timeout, so doesn't affect the + aggregate rate of exception logging, only the rate at which we record + ocurrences of a single exception, to prevent datastore contention. + """ + self.log_interval = log_interval + logging.Handler.__init__(self) + + @classmethod + def __RelativePath(cls, path): + """Rewrites a path to be relative to the app's root directory. + + Args: + path: The path to rewrite. + + Returns: + The path with the prefix removed, if that prefix matches the app's + root directory. + """ + cwd = os.getcwd() + if path.startswith(cwd): + path = path[len(cwd)+1:] + return path + + @classmethod + def __GetSignature(cls, exc_info): + """Returns a unique signature string for an exception. + + Args: + exc_info: The exc_info object for an exception. + + Returns: + A unique signature string for the exception, consisting of fully + qualified exception name and call site. + """ + ex_type, unused_value, trace = exc_info + frames = traceback.extract_tb(trace) + + fulltype = '%s.%s' % (ex_type.__module__, ex_type.__name__) + path, line_no = frames[-1][:2] + path = cls.__RelativePath(path) + site = '%s:%d' % (path, line_no) + signature = '%s@%s' % (fulltype, site) + if len(signature) > MAX_SIGNATURE_LENGTH: + signature = 'hash:%s' % sha.new(signature).hexdigest() + + return signature + + @classmethod + def __GetURL(cls): + """Returns the URL of the page currently being served. + + Returns: + The full URL of the page currently being served. + """ + if os.environ['SERVER_PORT'] == '80': + scheme = 'http://' + else: + scheme = 'https://' + host = os.environ['SERVER_NAME'] + script_name = urllib.quote(os.environ['SCRIPT_NAME']) + path_info = urllib.quote(os.environ['PATH_INFO']) + qs = os.environ.get('QUERY_STRING', '') + if qs: + qs = '?' + qs + return scheme + host + script_name + path_info + qs + + def __GetFormatter(self): + """Returns the log formatter for this handler. + + Returns: + The log formatter to use. + """ + if self.formatter: + return self.formatter + else: + return logging._defaultFormatter + + def emit(self, record): + """Log an error to the datastore, if applicable. + + Args: + The logging.LogRecord object. + See http://docs.python.org/library/logging.html#logging.LogRecord + """ + try: + if not record.exc_info: + return + + signature = self.__GetSignature(record.exc_info) + + if not memcache.add(signature, None, self.log_interval): + return + + db.run_in_transaction_custom_retries(1, self.__EmitTx, signature, + record.exc_info) + except Exception: + self.handleError(record) + + def __EmitTx(self, signature, exc_info): + """Run in a transaction to insert or update the record for this transaction. + + Args: + signature: The signature for this exception. + exc_info: The exception info record. + """ + today = datetime.date.today() + version = os.environ['CURRENT_VERSION_ID'] + major_ver, minor_ver = version.rsplit('.', 1) + minor_ver = int(minor_ver) + key_name = ExceptionRecord.get_key_name(signature, version) + + exrecord = ExceptionRecord.get_by_key_name(key_name) + if not exrecord: + exrecord = ExceptionRecord( + key_name=key_name, + signature=signature, + major_version=major_ver, + minor_version=minor_ver, + date=today, + stacktrace=self.__GetFormatter().formatException(exc_info), + http_method=os.environ['REQUEST_METHOD'], + url=self.__GetURL(), + handler=self.__RelativePath(os.environ['PATH_TRANSLATED'])) + + exrecord.count += 1 + exrecord.put() + + +def register_logger(logger=None): + if not logger: + logger = logging.getLogger() + handler = ExceptionRecordingHandler() + logger.addHandler(handler) + return handler