thirdparty/google_appengine/google/appengine/ext/ereporter/ereporter.py
changeset 2864 2e0b0af889be
--- /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