Commits

Ian Bicking committed a09a557

Added /var/log/silverlining/apps/APP_NAME/errors.log, which is a per-application error log for stderr and wsgi.errors output, plus an environmental variable SILVER_LOGS which points to that directory

Comments (0)

Files changed (8)

docs/envvariables.txt

     hostname possible.  When only one hostname is mapped to an
     application this is always correct.
 
+``SILVER_LOGS``:
+
+    This is a directory where log files should be kept.  This is a
+    per-application directory.  In development it will be in
+    ``silver-logs/`` under your application's main directory (what you
+    created with ``silver init``).
+
 ``CONFIG_*``:
 
     This is the naming convention for all the information coming from

docs/filelayout.txt

 
 Log files go in ``/var/log/silverlining/``.
 
-(Per-application log files, and other kinds of logs, still need to be
-filled out better.)
+Individual applications have log files in
+``/var/log/silverlining/apps/APP_NAME``, specifically ``errors.log``
+is anything written to stderr, stdout, or ``environ['wsgi.errors']``.
+These items are also grouped by request (all content written is
+buffered, written out in one chunk with a header/footer to help group
+it to a request).

silverlining/mgr-scripts/master-runner.py

 import os
 import re
 import urllib
+import threading
 from silversupport.appconfig import AppConfig
 
 #don't show DeprecationWarning in error.log
 
 found_app = None
 found_app_instance_name = None
+error_collector = None
 
 
 def application(environ, start_response):
     try:
-        get_app(environ)
-    except:
-        import traceback
-        bt = traceback.format_exc()
-        start_response('500 Server Error', [('Content-type', 'text/plain')])
-        lines = ['There was an error loading the application:', bt, '\nEnviron:']
-        for name, value in sorted(environ.items()):
-            try:
-                lines.append('%s=%r' % (name, value))
-            except:
-                lines.append('%s=<error>' % name)
-        return ['\n'.join(lines)]
-    return found_app(environ, start_response)
+        try:
+            get_app(environ)
+        except:
+            import traceback
+            bt = traceback.format_exc()
+            start_response('500 Server Error', [('Content-type', 'text/plain')])
+            lines = ['There was an error loading the application:', bt, '\nEnviron:']
+            for name, value in sorted(environ.items()):
+                try:
+                    lines.append('%s=%r' % (name, value))
+                except:
+                    lines.append('%s=<error>' % name)
+            return ['\n'.join(lines)]
+        return found_app(environ, start_response)
+    finally:
+        if error_collector is not None:
+            error_collector.flush_request(environ)
 
 
 def get_app(environ):
-    global found_app, found_app_instance_name
+    global found_app, found_app_instance_name, error_collector
     delete_boring_vars(environ)
     instance_name = environ['SILVER_INSTANCE_NAME']
     os.environ['SILVER_INSTANCE_NAME'] = instance_name
     app_config = AppConfig.from_instance_name(instance_name)
+    if error_collector is None:
+        log_location = os.path.join('/var/log/silverlining/apps', app_config.app_name)
+        if not os.path.exists(log_location):
+            os.makedirs(log_location)
+        error_collector = ErrorCollector(os.path.join(log_location, 'errors.log'))
+        sys.stderr = sys.stdout = error_collector
+    error_collector.start_request()
+    environ['silverlining.apache_errors'] = environ['wsgi.errors']
+    environ['wsgi.errors'] = error_collector
     os.environ['SILVER_CANONICAL_HOST'] = app_config.canonical_hostname()
     ## FIXME: give a real version here...
     environ['SILVER_VERSION'] = os.environ['SILVER_VERSION'] = 'silverlining/0.0'
     for name in BORING_VARS:
         if name in environ:
             del environ[name]
+
+
+class ErrorCollector(object):
+    def __init__(self, filename):
+        self.filename = filename
+        self.buffers = threading.local()
+
+    def start_request(self):
+        self.buffers.start_time = time.time()
+        self.buffers.buffer = []
+
+    def write(self, text):
+        self.buffers.buffer.append(text)
+
+    def writelines(self, lines):
+        self.buffers.buffer.extend(lines)
+
+    def flush(self):
+        ## FIXME: should this do something?
+        pass
+
+    def close(self):
+        ## FIXME: should this exist?
+        pass
+
+    def flush_request(self, environ):
+        if not self.buffers.buffer:
+            return
+        total = time.time() - self.buffers.start_time
+        buf = self.buffers.buffer
+        date_formatted = time.strftime('%Y-%d-%m %H:%M:%S', time.gmtime(self.buffers.start_time))
+        req_name = (
+            environ['REQUEST_METHOD'] + ' ' +
+            environ.get('SCRIPT_NAME', '') +
+            environ.get('PATH_INFO', ''))
+        buf.insert(0, 'Errors for request %s (%s, %fsec):\n'
+                   % (req_name, date_formatted, total))
+        if not buf[-1].endswith('\n'):
+            buf.append('\n')
+        buf.append('Finish errors for request %s (%s)\n' % (req_name, date_formatted))
+        complete = ''.join(buf)
+        fp = open(self.filename, 'a')
+        fp.write(complete)
+        fp.flush()
+        fp.close()
+        self.buffers.buffer[:] = []
+
+    def __repr__(self):
+        return '<silverlining error collector %s>' % self.filename

silverlining/mgr-scripts/update-service.py

                 os.unlink(fn)
     else:
         os.makedirs(tmp)
+    if not os.path.exists(app_config.log_dir):
+        run(['sudo', '-u', 'www-data', 'mkdir -p %s' % app_config.log_dir])
 
 
 if __name__ == '__main__':

silverlining/server-sync-scripts/update-server-script.sh

 mkdir -p /var/silverlining/build-files
 mkdir -p /var/log/silverlining-setup
 mkdir -p /var/log/silverlining
+mkdir -p /var/log/silverlining/apps
+chown www-data:www-data /var/log/silverlining/apps
 mkdir -p /var/www
 mkdir -p /var/lib/silverlining
 mkdir -p /var/lib/silverlining/backups

silversupport/appconfig.py

             tmp = environ['TEMP'] = os.path.join('/var/lib/silverlining/tmp/', self.app_name)
             if not os.path.exists(tmp):
                 os.makedirs(tmp)
+        environ['SILVER_LOGS'] = self.log_dir
+        if not is_production and not os.path.exists(environ['SILVER_LOGS']):
+            os.makedirs(environ['SILVER_LOGS'])
         return environ
 
+    @property
+    def log_dir(self):
+        if is_production():
+            return os.path.join('/var/log/silverlining/apps', self.app_name)
+        else:
+            return os.path.join(self.app_dir, 'silver-logs')
+
     def install_services(self, clear=False):
         """Installs all the services for this application.
 

tests/functional/example-app/example_app.py

 import os
+import sys
 import traceback
 
 
         else:
             body = 'INSTANCE=%s' % os.environ['SILVER_INSTANCE_NAME']
         start_response('200 OK', [('Content-type', 'text/plain')])
+        environ['wsgi.errors'].write('Executed application\n')
+        print 'This is stdout'
+        print >> sys.stderr, 'This is stderr'
         return [body]
     except:
         start_response('500 Server Error', [('Content-type', 'text/plain')])

tests/functional/runtest.py

 import re
 import urllib
 from scripttest import TestFileEnvironment
+from silversupport.shell import ssh
 
 here = os.path.dirname(os.path.abspath(__file__))
 
     return env
 
 stage_seq = ['create-node', 'setup-node', 'clean', 'update', 'update-path',
-             'query', 'activation', 'backup-update', 'backup-clear']
+             'logs', 'query', 'activation', 'backup-update', 'backup-clear']
 
 
 def run_stage(name, match):
             print 'The actual HTTP response: (%s)' % url
             print resp
 
+        if run_stage(stage, 'logs'):
+            print 'Doing log check'
+            ssh('www-data', name, 'rm /var/log/silverlining/apps/functest/*')
+            url = 'http://%s/test/update' % name
+            resp = urllib.urlopen(url).read()
+            text, _, _ = ssh('www-data', name,
+                             'cat /var/log/silverlining/apps/functest/errors.log',
+                             capture_stdout=True)
+            text_lines = ''.join(text.strip().splitlines(True)[1:-1]).strip()
+            assert text_lines == """\
+Executed application
+This is stdout
+This is stderr""", repr(text_lines)
+
         if run_stage(stage, 'query'):
             print 'Doing query'
             result = env.run('silver --yes query %s' % name)
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.