Ian Bicking avatar Ian Bicking committed 9004431

lots of styling updates

Comments (0)

Files changed (6)

silverlog/__init__.py

 import os
 import re
 import json
+import time
 import tempita
 from webob.dec import wsgify
 from webob import exc
         result = dict(
             path=log.path, group=log.group,
             id=log.id, description=log.description,
-            content=log.content())
+            content=log.content(),
+            chunks=log.parse_chunks(),
+            log_type=log.parser)
         return json_response(result)
 
 NAMES = [
-    (r'^SILVER_DIR/apps/(?P<app>[^/]+)/stderr.log$',
-     ('{{app}}', '{{app}}: stderr')),
+    (r'^SILVER_DIR/apps/(?P<app>[^/]+)/error.log$',
+     ('{{app}}', '{{app}}: error log'),
+     'silver_error_log'),
     (r'^SILVER_DIR/apps/(?P<app>[^/]+)/(?P<name>.*)$',
-     ('{{app}}', '{{app}}: {{name}}')),
+     ('{{app}}', '{{app}}: {{name}}'),
+     'generic_log'),
     (r'^APACHE_DIR/access.log$',
-     ('system', 'Apache access log')),
+     ('system', 'Apache access log'),
+     'apache_access_log'),
     (r'^APACHE_DIR/error.log$',
-     ('system', 'Apache error log')),
+     ('system', 'Apache error log'),
+     'apache_error_log'),
     (r'^APACHE_DIR/rewrite.log$',
-     ('system', 'Apache rewrite log')),
+     ('system', 'Apache rewrite log'),
+     'apache_rewrite_log'),
     (r'^SILVER_DIR/setup-node.log',
-     ('system', 'silver setup-node log')),
+     ('system', 'silver setup-node log'),
+     'silver_setup_node_log'),
     ]
 
 DEFAULT_DIRS = {
     def read(self):
         self.logs = {}
         patterns = []
-        for pattern, result in NAMES:
+        for pattern, result, parser in NAMES:
             for dirname, dirpath in self.dirs.items():
                 pattern = pattern.replace(dirname, dirpath)
-            patterns.append((pattern, result))
+            patterns.append((pattern, result, parser))
         for filename in walk_files(self.dirs.values()):
-            for pattern, (group, description) in patterns:
+            for pattern, (group, description), parser in patterns:
                 match = re.match(pattern, filename)
                 if match:
                     vars = match.groupdict()
                     vars['log_set'] = self
                     description = tempita.Template(description).substitute(vars)
                     group = tempita.Template(group).substitute(vars)
-                    log = Log(filename, group, description)
+                    log = Log(filename, group, description, parser)
                     self.logs.setdefault(group, {})[filename] = log
                     break
             else:
                 yield filename
 
 class Log(object):
-    def __init__(self, path, group, description):
+    def __init__(self, path, group, description, parser):
         self.path = path
         self.group = group
         self.description = description
+        self.parser = parser
 
     def __repr__(self):
         return '<%s %s: %r>' % (
         fp.close()
         return c
 
+    def parse_chunks(self):
+        method = getattr(self, self.parser)
+        return method()
+
+    def generic_log(self):
+        fp = open(self.path)
+        l = []
+        for line in fp:
+            l.append({'data': line.strip()})
+        return l
+
+    silver_error_log = generic_log
+    apache_rewrite_log = generic_log
+
+    def apache_access_log(self):
+        fp = open(self.path)
+        l = []
+        regex = re.compile(
+            r'''
+            ^
+            (?P<ip>-|[0-9.]+?)
+              \s+
+            - \s+
+            - \s+
+            \[(?P<date>[^\]]+?)\]
+              \s+
+            "(?P<method>[A-Z]+)
+              \s+
+            (?P<path>[^ ]+)
+              \s+
+            (?P<http_version>[^"]+)"
+              \s+
+            (?P<response_code>\d+)
+              \s+
+            (?P<response_bytes>-|\d+)
+              \s+
+            "(?P<referrer>[^"]*)"
+              \s+
+            "(?P<user_agent>[^"]*)"
+            (?:
+              \s+
+            (?P<host>[^ ]+)
+              \s+
+            (?P<app_name>[^ ]+)
+              \s+
+            (?P<milliseconds>\d+)
+            )?
+            ''', re.VERBOSE)
+        for line in fp:
+            line = line.strip()
+            match = regex.match(line)
+            if match:
+                data = match.groupdict()
+                data['date'] = self._translate_apache_date(data['date'])
+            else:
+                data = {}
+            data['data'] = line
+            l.append(data)
+        return l
+
+    @staticmethod
+    def _translate_apache_date(date):
+        return time.strftime('%B %d, %Y %H:%M:%S', time.strptime(date.split()[0], '%d/%b/%Y:%H:%M:%S'))
+
+    def apache_error_log(self):
+        fp = open(self.path)
+        l = []
+        regex = re.compile(
+            r'''
+            \[(?P<date>[^\]]+)]
+              \s+
+            \[(?P<level>[^\]]+)]
+              \s
+            (?:
+              \[client \s+ (?P<remote_addr>[0-9.]+)\]
+                \s
+            )?
+            (?P<message>.*)
+            ''', re.VERBOSE)
+        last_data = None
+        for line in fp:
+            line = line.strip()
+            match = regex.match(line)
+            if match:
+                data = match.groupdict()
+                if (last_data and data['date'] == last_data['date']
+                    and data['level'] == last_data['level']
+                    and data['remote_addr'] == last_data['remote_addr']):
+                    last_data['data'] += '\n' + line
+                    last_data['message'] += '\n' + data['message']
+                    continue
+            else:
+                data = {}
+            data['data'] = line
+            last_data = data
+            l.append(data)
+        return l
+
+    def silver_setup_node_log(self):
+        fp = open(self.path)
+        l = []
+        rerun = re.compile(
+            r'''
+            Rerun \s+ setup-node \s+ on \s+ (?P<date>.*)
+            ''', re.VERBOSE | re.I)
+        run_by = re.compile(
+            r'''
+            Run \s+ by: \s* (?P<username>.*)
+            ''', re.VERBOSE | re.I)
+        last_item = {}
+        for line in fp:
+            line = line.strip()
+            last_item.setdefault('data', []).append(line)
+            match = rerun.match(line)
+            if match:
+                last_item.update(match.groupdict())
+                continue
+            match = run_by.match(line)
+            if match:
+                last_item.update(match.groupdict())
+                continue
+            if line == '-'*len(line):
+                last_item['data'] = '\n'.join(last_item['data'])
+                l.append(last_item)
+                last_item = {}
+                continue
+            last_item.setdefault('messages', []).append(line)
+        if last_item:
+            last_item['data'] = '\n'.join(last_item['data'])
+            l.append(last_item)
+        return l
+
     @property
     def id(self):
         id = self.path.replace('/', '_').strip('_')

silverlog/static/index.html

 <html>
 <head>
 <link href="/style.css" type="text/css" rel="stylesheet" />
+<!-- <script
+src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
+-->
 <script
-src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
+src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script>
 <script src="/sammy.js"></script>
+<script src="/pure.js"></script>
+<script src="/jquery.relatize_date.js"></script>
 <script src="/script.js"></script>
-<script src="/pure.js"></script>
 <title>Silver Logs</title>
 </head>
 <body>
 
 </div><!-- /#body -->
 
+<div style="display: none">
+
+  <div class="template-default">
+    <div class="log-section">
+      <code class="log-line"></code>
+    </div>
+  </div>
+
+  <div class="template-apache_access_log">
+    <table>
+      <tr class="log-section-header">
+        <th>Date</th>
+        <th>Method</th>
+        <th>Path</th>
+        <th>Code</th>
+        <th>Bytes</th>
+        <th>Referrer</th>
+        <th>User-Agent</th>
+        <th>Host</th>
+        <th>App</th>
+        <th>Time</th>
+      </tr>
+      <tr class="log-section">
+        <td class="date log-date"></td>
+        <td class="log-method"></td>
+        <td><code class="log-path"></code></td>
+        <td class="log-response_code"></td>
+        <td class="log-response_bytes"></td>
+        <td><a href="" class="log-referrer"></a></td>
+        <td class="log-user_agent"></td>
+        <td><a href="" class="log-host"></a></td>
+        <td class="log-app_name"></td>
+        <td class="log-milliseconds"></td>
+      </tr>
+    </table>
+  </div>
+
+  <div class="template-apache_error_log">
+    <div class="log-section">
+      <div class="log-section-header">
+        <span class="log-warning-level">level</span>
+        <span class="date log-date">date</span>
+        <code class="log-client"></code>
+      </div>
+      <pre class="log-message"></pre>
+    </div>
+  </div>
+
+</div><!-- /templates -->
+
 </body>
 </html>

silverlog/static/jquery.relatize_date.js

+// All credit goes to Rick Olson.
+(function($) {
+  $.fn.relatizeDate = function() {
+    return $(this).each(function() {
+      $(this).text( $.relatizeDate(this) )
+    })
+  }
+
+  $.relatizeDate = function(element) {
+    return $.relatizeDate.timeAgoInWords( new Date($(element).text()) )
+  }
+
+  // shortcut
+  $r = $.relatizeDate
+
+  $.extend($.relatizeDate, {
+    shortDays: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ],
+    days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+    shortMonths: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ],
+    months: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ],
+
+    /**
+     * Given a formatted string, replace the necessary items and return.
+     * Example: Time.now().strftime("%B %d, %Y") => February 11, 2008
+     * @param {String} format The formatted string used to format the results
+     */
+    strftime: function(date, format) {
+      var day = date.getDay(), month = date.getMonth();
+      var hours = date.getHours(), minutes = date.getMinutes();
+
+      var pad = function(num) {
+        var string = num.toString(10);
+        return new Array((2 - string.length) + 1).join('0') + string
+      };
+
+      return format.replace(/\%([aAbBcdHImMpSwyY])/g, function(part) {
+        switch(part[1]) {
+          case 'a': return $r.shortDays[day]; break;
+          case 'A': return $r.days[day]; break;
+          case 'b': return $r.shortMonths[month]; break;
+          case 'B': return $r.months[month]; break;
+          case 'c': return date.toString(); break;
+          case 'd': return pad(date.getDate()); break;
+          case 'H': return pad(hours); break;
+          case 'I': return pad((hours + 12) % 12); break;
+          case 'm': return pad(month + 1); break;
+          case 'M': return pad(minutes); break;
+          case 'p': return hours > 12 ? 'PM' : 'AM'; break;
+          case 'S': return pad(date.getSeconds()); break;
+          case 'w': return day; break;
+          case 'y': return pad(date.getFullYear() % 100); break;
+          case 'Y': return date.getFullYear().toString(); break;
+        }
+      })
+    },
+
+    timeAgoInWords: function(targetDate, includeTime) {
+      return $r.distanceOfTimeInWords(targetDate, new Date(), includeTime);
+    },
+
+    /**
+     * Return the distance of time in words between two Date's
+     * Example: '5 days ago', 'about an hour ago'
+     * @param {Date} fromTime The start date to use in the calculation
+     * @param {Date} toTime The end date to use in the calculation
+     * @param {Boolean} Include the time in the output
+     */
+    distanceOfTimeInWords: function(fromTime, toTime, includeTime) {
+      var delta = parseInt((toTime.getTime() - fromTime.getTime()) / 1000);
+      if (delta < 60) {
+          return 'less than a minute ago';
+      } else if (delta < 120) {
+          return 'about a minute ago';
+      } else if (delta < (45*60)) {
+          return (parseInt(delta / 60)).toString() + ' minutes ago';
+      } else if (delta < (120*60)) {
+          return 'about an hour ago';
+      } else if (delta < (24*60*60)) {
+          return 'about ' + (parseInt(delta / 3600)).toString() + ' hours ago';
+      } else if (delta < (48*60*60)) {
+          return '1 day ago';
+      } else {
+        var days = (parseInt(delta / 86400)).toString();
+        if (days > 5) {
+          var fmt  = '%B %d, %Y'
+          if (includeTime) fmt += ' %I:%M %p'
+          return $r.strftime(fromTime, fmt);
+        } else {
+          return days + " days ago"
+        }
+      }
+    }
+  })
+})(jQuery);

silverlog/static/script.js

       url: "/api/log/"+this.params.log_id,
       dataType: "json",
       success: function (result) {
+        var log_type = result.log_type;
+        if (! templateRules[log_type]) {
+          log_type = "default";
+        }
         $('#header #title-slot').text(result.description);
-        $('#log-view').render(
+        $('#log-view .log-view').remove();
+        var tmpl = $('.template-'+log_type);
+        tmpl = tmpl.clone();
+        tmpl.removeClass('template-'+log_type);
+        tmpl.show();
+        tmpl.addClass('log-view');
+        $('#log-view').append(tmpl);
+        console.log('rule', templateRules[log_type]);
+        console.log('data', result);
+        tmpl = tmpl.render(
           result,
-          {
-            "pre.content": "content"
-          });
+          templateRules[log_type]);
+        $('.date', tmpl).relatizeDate();
       }
     });
   });
   app.run('#/');
 });
 
+templateRules = {
+
+  "default": {
+    "div.log-section": {
+      "chunk<-chunks": {
+        "code.log-line": "chunk.data"
+      }
+    }
+  },
+
+  "apache_access_log": {
+    "tr.log-section": {
+      "chunk<-chunks": {
+        ".log-date": "chunk.date",
+        ".log-method": "chunk.method",
+        ".log-path": "chunk.path",
+        ".log-response_code": "chunk.response_code",
+        ".log-response_bytes": "chunk.response_bytes",
+        ".log-referrer": function (ctx) {return ctx.item.referrer == "-" ? "" : ctx.item.referrer},
+        ".log-referrer@href": function (ctx) {return ctx.item.referrer == "-" ? "" : ctx.item.referrer},
+        ".log-user_agent": "chunk.user_agent",
+        ".log-host": "chunk.host",
+        ".log-host@href": function (ctx) {return "http://"+ctx.item.host;},
+        ".log-app_name": "chunk.app_name",
+        ".log-milliseconds": "chunk.milliseconds"
+      }
+    }
+  },
+
+  "apache_error_log": {
+    "div.log-section": {
+      "chunk<-chunks": {
+        ".log-warning-level": "chunk.level",
+        ".log-warning-level@class+": function (ctx) {return " log-warning-level-"+ctx.item.level},
+        ".log-date": "chunk.date",
+        ".log-client": "chunk.remote_addr",
+        ".log-message": "chunk.message"
+      }
+    }
+  }
+
+};
+
 var currentScreen = null;
 
 function moveScreen(selector) {

silverlog/static/style.css

 pre.content {
   overflow: auto;
 }
+
+code.log-line {
+  white-space: pre-wrap;
+}
+
+div.log-section {
+  border-bottom: 1px dotted #aaa;
+}
+
+div.log-section-header {
+  border-bottom: 1px dotted #eee;
+  font-size: 80%;
+  margin: 0;
+}
+
+.log-warning-level-notice {
+  background-color: #ff9;
+}
+
+.log-warning-level-warn {
+  background-color: #fd9;
+}
+
+.log-warning-level-error {
+  background-color: #f99;
+}
+
+.log-warning-level-crit {
+  background-color: #f77;
+}
+
+.log-user_agent {
+  font-size: 70%;
+}
+
+.log-date {
+  font-size: 70%;
+}
+
+.log-response_bytes {
+  text-align: right;
+}

tests/test_logger.py

 wsgi_app = ns['application']
 app = TestApp(wsgi_app)
 
+here_id = os.path.dirname(os.path.dirname(__file__)).replace('/', '_').strip('_')
+
 def test_app():
-    assert wsgi_app.log_set.dirs
-    resp = app.get('/')
-    resp.mustcontain(
-        'setup-node.log', 'rewrite.log')
-    resp = resp.click(href=r'/view/system\?path=.*rewrite\.log')
-    print resp
+    resp = app.get('/api/list-logs')
+    assert resp.json
+    resp = app.get('/api/log/%s_tests_test-logs_apache2_access.log'
+                   % here_id)
+    assert resp.json
+    import pprint
+    pprint.pprint(resp.json['chunks'][0])
+    assert 0
 
 if __name__ == '__main__':
     test_app()
-    
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.