thirdparty/google_appengine/google/appengine/ext/ereporter/ereporter.py
changeset 2864 2e0b0af889be
equal deleted inserted replaced
2862:27971a13089f 2864:2e0b0af889be
       
     1 #!/usr/bin/env python
       
     2 #
       
     3 # Copyright 2007 Google Inc.
       
     4 #
       
     5 # Licensed under the Apache License, Version 2.0 (the "License");
       
     6 # you may not use this file except in compliance with the License.
       
     7 # You may obtain a copy of the License at
       
     8 #
       
     9 #     http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    11 # Unless required by applicable law or agreed to in writing, software
       
    12 # distributed under the License is distributed on an "AS IS" BASIS,
       
    13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       
    14 # See the License for the specific language governing permissions and
       
    15 # limitations under the License.
       
    16 #
       
    17 
       
    18 """A logging handler that records information about unique exceptions.
       
    19 
       
    20 'Unique' in this case is defined as a given (exception class, location) tuple.
       
    21 Unique exceptions are logged to the datastore with an example stacktrace and an
       
    22 approximate count of occurrences, grouped by day and application version.
       
    23 
       
    24 A cron handler, in google.appengine.ext.ereporter.report_generator, constructs
       
    25 and emails a report based on the previous day's exceptions.
       
    26 
       
    27 Example usage:
       
    28 
       
    29 In your handler script(s), add:
       
    30 
       
    31   import logging
       
    32   from google.appengine.ext import ereporter
       
    33 
       
    34   ereporter.register_logger()
       
    35 
       
    36 In your app.yaml, add:
       
    37 
       
    38   handlers:
       
    39   - url: /_ereporter/.*
       
    40     script: $PYTHON_LIB/google/appengine/ext/ereporter/report_generator.py
       
    41     login: admin
       
    42 
       
    43 In your cron.yaml, add:
       
    44 
       
    45   cron:
       
    46   - description: Daily exception report
       
    47     url: /_ereporter?sender=you@yourdomain.com
       
    48     schedule: every day 00:00
       
    49 
       
    50 This will cause a daily exception report to be generated and emailed to all
       
    51 admins, with exception traces grouped by minor version. If you only want to
       
    52 get exception information for the most recent minor version, add the
       
    53 'versions=latest' argument to the query string. For other valid query string
       
    54 arguments, see report_generator.py.
       
    55 
       
    56 If you anticipate a lot of exception traces (for example, if you're deploying
       
    57 many minor versions, each of which may have its own set of exceptions), you
       
    58 can ensure that the traces from the newest minor versions get included by adding
       
    59 this to your index.yaml:
       
    60 
       
    61   indexes:
       
    62   - kind: __google_ExceptionRecord
       
    63     properties:
       
    64     - name: date
       
    65     - name: major_version
       
    66     - name: minor_version
       
    67       direction: desc
       
    68 """
       
    69 
       
    70 
       
    71 
       
    72 
       
    73 
       
    74 import datetime
       
    75 import logging
       
    76 import os
       
    77 import sha
       
    78 import traceback
       
    79 import urllib
       
    80 
       
    81 from google.appengine.api import memcache
       
    82 from google.appengine.ext import db
       
    83 from google.appengine.ext import webapp
       
    84 
       
    85 
       
    86 MAX_SIGNATURE_LENGTH = 256
       
    87 
       
    88 
       
    89 class ExceptionRecord(db.Model):
       
    90   """Datastore model for a record of a unique exception."""
       
    91 
       
    92   signature = db.StringProperty(required=True)
       
    93   major_version = db.StringProperty(required=True)
       
    94   minor_version = db.IntegerProperty(required=True)
       
    95   date = db.DateProperty(required=True)
       
    96   count = db.IntegerProperty(required=True, default=0)
       
    97 
       
    98   stacktrace = db.TextProperty(required=True)
       
    99   http_method = db.TextProperty(required=True)
       
   100   url = db.TextProperty(required=True)
       
   101   handler = db.TextProperty(required=True)
       
   102 
       
   103   @classmethod
       
   104   def get_key_name(cls, signature, version, date=None):
       
   105     """Generates a key name for an exception record.
       
   106 
       
   107     Args:
       
   108       signature: A signature representing the exception and its site.
       
   109       version: The major/minor version of the app the exception occurred in.
       
   110       date: The date the exception occurred.
       
   111 
       
   112     Returns:
       
   113       The unique key name for this exception record.
       
   114     """
       
   115     if not date:
       
   116       date = datetime.date.today()
       
   117     return '%s@%s:%s' % (signature, date, version)
       
   118 
       
   119 
       
   120 class ExceptionRecordingHandler(logging.Handler):
       
   121   """A handler that records exception data to the App Engine datastore."""
       
   122 
       
   123   def __init__(self, log_interval=10):
       
   124     """Constructs a new ExceptionRecordingHandler.
       
   125 
       
   126     Args:
       
   127       log_interval: The minimum interval at which we will log an individual
       
   128         exception. This is a per-exception timeout, so doesn't affect the
       
   129         aggregate rate of exception logging, only the rate at which we record
       
   130         ocurrences of a single exception, to prevent datastore contention.
       
   131     """
       
   132     self.log_interval = log_interval
       
   133     logging.Handler.__init__(self)
       
   134 
       
   135   @classmethod
       
   136   def __RelativePath(cls, path):
       
   137     """Rewrites a path to be relative to the app's root directory.
       
   138 
       
   139     Args:
       
   140       path: The path to rewrite.
       
   141 
       
   142     Returns:
       
   143       The path with the prefix removed, if that prefix matches the app's
       
   144         root directory.
       
   145     """
       
   146     cwd = os.getcwd()
       
   147     if path.startswith(cwd):
       
   148       path = path[len(cwd)+1:]
       
   149     return path
       
   150 
       
   151   @classmethod
       
   152   def __GetSignature(cls, exc_info):
       
   153     """Returns a unique signature string for an exception.
       
   154 
       
   155     Args:
       
   156       exc_info: The exc_info object for an exception.
       
   157 
       
   158     Returns:
       
   159       A unique signature string for the exception, consisting of fully
       
   160       qualified exception name and call site.
       
   161     """
       
   162     ex_type, unused_value, trace = exc_info
       
   163     frames = traceback.extract_tb(trace)
       
   164 
       
   165     fulltype = '%s.%s' % (ex_type.__module__, ex_type.__name__)
       
   166     path, line_no = frames[-1][:2]
       
   167     path = cls.__RelativePath(path)
       
   168     site = '%s:%d' % (path, line_no)
       
   169     signature = '%s@%s' % (fulltype, site)
       
   170     if len(signature) > MAX_SIGNATURE_LENGTH:
       
   171       signature = 'hash:%s' % sha.new(signature).hexdigest()
       
   172 
       
   173     return signature
       
   174 
       
   175   @classmethod
       
   176   def __GetURL(cls):
       
   177     """Returns the URL of the page currently being served.
       
   178 
       
   179     Returns:
       
   180       The full URL of the page currently being served.
       
   181     """
       
   182     if os.environ['SERVER_PORT'] == '80':
       
   183       scheme = 'http://'
       
   184     else:
       
   185       scheme = 'https://'
       
   186     host = os.environ['SERVER_NAME']
       
   187     script_name = urllib.quote(os.environ['SCRIPT_NAME'])
       
   188     path_info = urllib.quote(os.environ['PATH_INFO'])
       
   189     qs = os.environ.get('QUERY_STRING', '')
       
   190     if qs:
       
   191       qs = '?' + qs
       
   192     return scheme + host + script_name + path_info + qs
       
   193 
       
   194   def __GetFormatter(self):
       
   195     """Returns the log formatter for this handler.
       
   196 
       
   197     Returns:
       
   198       The log formatter to use.
       
   199     """
       
   200     if self.formatter:
       
   201       return self.formatter
       
   202     else:
       
   203       return logging._defaultFormatter
       
   204 
       
   205   def emit(self, record):
       
   206     """Log an error to the datastore, if applicable.
       
   207 
       
   208     Args:
       
   209       The logging.LogRecord object.
       
   210         See http://docs.python.org/library/logging.html#logging.LogRecord
       
   211     """
       
   212     try:
       
   213       if not record.exc_info:
       
   214         return
       
   215 
       
   216       signature = self.__GetSignature(record.exc_info)
       
   217 
       
   218       if not memcache.add(signature, None, self.log_interval):
       
   219         return
       
   220 
       
   221       db.run_in_transaction_custom_retries(1, self.__EmitTx, signature,
       
   222                                            record.exc_info)
       
   223     except Exception:
       
   224       self.handleError(record)
       
   225 
       
   226   def __EmitTx(self, signature, exc_info):
       
   227     """Run in a transaction to insert or update the record for this transaction.
       
   228 
       
   229     Args:
       
   230       signature: The signature for this exception.
       
   231       exc_info: The exception info record.
       
   232     """
       
   233     today = datetime.date.today()
       
   234     version = os.environ['CURRENT_VERSION_ID']
       
   235     major_ver, minor_ver = version.rsplit('.', 1)
       
   236     minor_ver = int(minor_ver)
       
   237     key_name = ExceptionRecord.get_key_name(signature, version)
       
   238 
       
   239     exrecord = ExceptionRecord.get_by_key_name(key_name)
       
   240     if not exrecord:
       
   241       exrecord = ExceptionRecord(
       
   242           key_name=key_name,
       
   243           signature=signature,
       
   244           major_version=major_ver,
       
   245           minor_version=minor_ver,
       
   246           date=today,
       
   247           stacktrace=self.__GetFormatter().formatException(exc_info),
       
   248           http_method=os.environ['REQUEST_METHOD'],
       
   249           url=self.__GetURL(),
       
   250           handler=self.__RelativePath(os.environ['PATH_TRANSLATED']))
       
   251 
       
   252     exrecord.count += 1
       
   253     exrecord.put()
       
   254 
       
   255 
       
   256 def register_logger(logger=None):
       
   257   if not logger:
       
   258     logger = logging.getLogger()
       
   259   handler = ExceptionRecordingHandler()
       
   260   logger.addHandler(handler)
       
   261   return handler