|
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 """Generates and emails daily exception reports. |
|
19 |
|
20 See google/appengine/ext/ereporter/__init__.py for usage details. |
|
21 |
|
22 Valid query string arguments to the report_generator script include: |
|
23 delete: Set to 'false' to prevent deletion of exception records from the |
|
24 datastore after sending a report. Defaults to 'true'. |
|
25 debug: Set to 'true' to return the report in the response instead of |
|
26 emailing it. |
|
27 date: The date to generate the report for, in yyyy-mm-dd format. Defaults to |
|
28 yesterday's date. Useful for debugging. |
|
29 max_results: Maximum number of entries to include in a report. |
|
30 sender: The email address to use as the sender. Must be an administrator. |
|
31 to: If specified, send reports to this address. If not specified, all |
|
32 admins are sent the report. |
|
33 versions: 'all' to report on all minor versions, or 'latest' for the latest. |
|
34 """ |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 import datetime |
|
41 import itertools |
|
42 import os |
|
43 import re |
|
44 from xml.sax import saxutils |
|
45 |
|
46 from google.appengine.api import mail |
|
47 from google.appengine.ext import db |
|
48 from google.appengine.ext import ereporter |
|
49 from google.appengine.ext import webapp |
|
50 from google.appengine.ext.webapp import template |
|
51 from google.appengine.ext.webapp.util import run_wsgi_app |
|
52 |
|
53 |
|
54 def isTrue(val): |
|
55 """Determines if a textual value represents 'true'. |
|
56 |
|
57 Args: |
|
58 val: A string, which may be 'true', 'yes', 't', '1' to indicate True. |
|
59 Returns: |
|
60 True or False |
|
61 """ |
|
62 val = val.lower() |
|
63 return val == 'true' or val == 't' or val == '1' or val == 'yes' |
|
64 |
|
65 |
|
66 class ReportGenerator(webapp.RequestHandler): |
|
67 """Handler class to generate and email an exception report.""" |
|
68 |
|
69 DEFAULT_MAX_RESULTS = 100 |
|
70 |
|
71 def __init__(self, send_mail=mail.send_mail, |
|
72 mail_admins=mail.send_mail_to_admins): |
|
73 super(ReportGenerator, self).__init__() |
|
74 |
|
75 self.send_mail = send_mail |
|
76 self.send_mail_to_admins = mail_admins |
|
77 |
|
78 def GetQuery(self, order=None): |
|
79 """Creates a query object that will retrieve the appropriate exceptions. |
|
80 |
|
81 Returns: |
|
82 A query to retrieve the exceptions required. |
|
83 """ |
|
84 q = ereporter.ExceptionRecord.all() |
|
85 q.filter('date =', self.yesterday) |
|
86 q.filter('major_version =', self.major_version) |
|
87 if self.version_filter.lower() == 'latest': |
|
88 q.filter('minor_version =', self.minor_version) |
|
89 if order: |
|
90 q.order(order) |
|
91 return q |
|
92 |
|
93 def GenerateReport(self, exceptions): |
|
94 """Generates an HTML exception report. |
|
95 |
|
96 Args: |
|
97 exceptions: A list of ExceptionRecord objects. This argument will be |
|
98 modified by this function. |
|
99 Returns: |
|
100 An HTML exception report. |
|
101 """ |
|
102 exceptions.sort(key=lambda e: (e.minor_version, -e.count)) |
|
103 versions = [(minor, list(excs)) for minor, excs |
|
104 in itertools.groupby(exceptions, lambda e: e.minor_version)] |
|
105 |
|
106 template_values = { |
|
107 'version_filter': self.version_filter, |
|
108 'version_count': len(versions), |
|
109 |
|
110 'exception_count': sum(len(excs) for _, excs in versions), |
|
111 |
|
112 'occurrence_count': sum(y.count for x in versions for y in x[1]), |
|
113 'app_id': self.app_id, |
|
114 'major_version': self.major_version, |
|
115 'date': self.yesterday, |
|
116 'versions': versions, |
|
117 } |
|
118 path = os.path.join(os.path.dirname(__file__), 'templates', 'report.html') |
|
119 return template.render(path, template_values) |
|
120 |
|
121 def SendReport(self, report): |
|
122 """Emails an exception report. |
|
123 |
|
124 Args: |
|
125 report: A string containing the report to send. |
|
126 """ |
|
127 subject = ('Daily exception report for app "%s", major version "%s"' |
|
128 % (self.app_id, self.major_version)) |
|
129 report_text = saxutils.unescape(re.sub('<[^>]+>', '', report)) |
|
130 mail_args = { |
|
131 'sender': self.sender, |
|
132 'subject': subject, |
|
133 'body': report_text, |
|
134 'html': report, |
|
135 } |
|
136 if self.to: |
|
137 mail_args['to'] = self.to |
|
138 self.send_mail(**mail_args) |
|
139 else: |
|
140 self.send_mail_to_admins(**mail_args) |
|
141 |
|
142 def get(self): |
|
143 self.version_filter = self.request.GET.get('versions', 'all') |
|
144 self.sender = self.request.GET['sender'] |
|
145 self.to = self.request.GET.get('to', None) |
|
146 report_date = self.request.GET.get('date', None) |
|
147 if report_date: |
|
148 self.yesterday = datetime.date(*[int(x) for x in report_date.split('-')]) |
|
149 else: |
|
150 self.yesterday = datetime.date.today() - datetime.timedelta(days=1) |
|
151 self.app_id = os.environ['APPLICATION_ID'] |
|
152 version = os.environ['CURRENT_VERSION_ID'] |
|
153 self.major_version, self.minor_version = version.rsplit('.', 1) |
|
154 self.minor_version = int(self.minor_version) |
|
155 self.max_results = int(self.request.GET.get('max_results', |
|
156 self.DEFAULT_MAX_RESULTS)) |
|
157 self.debug = isTrue(self.request.GET.get('debug', 'false')) |
|
158 self.delete = isTrue(self.request.GET.get('delete', 'true')) |
|
159 |
|
160 try: |
|
161 exceptions = self.GetQuery(order='-minor_version').fetch(self.max_results) |
|
162 except db.NeedIndexError: |
|
163 exceptions = self.GetQuery().fetch(self.max_results) |
|
164 |
|
165 if exceptions: |
|
166 report = self.GenerateReport(exceptions) |
|
167 if self.debug: |
|
168 self.response.out.write(report) |
|
169 else: |
|
170 self.SendReport(report) |
|
171 |
|
172 if self.delete: |
|
173 db.delete(exceptions) |
|
174 |
|
175 |
|
176 application = webapp.WSGIApplication([('.*', ReportGenerator)]) |
|
177 |
|
178 |
|
179 def main(): |
|
180 run_wsgi_app(application) |
|
181 |
|
182 |
|
183 if __name__ == '__main__': |
|
184 main() |