"""
Tornado server utilities

- LoggingApplication: a base Application class with logging and metrics
- RequestHandler: a base request handler with helpers

Dependencies:
- metrics
- servicelog

See tests/test_tornadoutil.py for usage example.

"""

import json
import datetime
import uuid
import httplib # for httplib.responses

import tornado.web
import tornado.options
import tornado.httpserver
import metrics
import servicelog


REQUEST_ID_HEADER = 'X-Request-Id'
AUTH_USER_HEADER = 'X-Auth-User'


class LoggingApplication(tornado.web.Application):
    """
    Overrides base log_request method to log requests to JSON
    UDP collector. Logs method, uri, remote_ip, etc.

    Also supports logging arbitrary key/value pair logging via a handler's
    'logvalues' attribute.

    >>> app = LoggingApplication('myservice')
    """
    def __init__(self, service_id, *args, **kwargs):
        self.service_id = service_id or 'undefined'
        metrics.configure(prefix=self.service_id)
        super(LoggingApplication, self).__init__(*args, **kwargs)

    def run(self, port): # pragma: no coverage
        """
        Run on given port. Parse standard options and start the http server.
        """
        tornado.options.parse_command_line()
        http_server = tornado.httpserver.HTTPServer(self)
        http_server.listen(port)
        tornado.ioloop.IOLoop.instance().start()

    def log_request(self, handler):
        """
        Override base method to log requests to JSON UDP collector and emit
        a metric.
        """
        packet = {'method': handler.request.method,
                  'uri': handler.request.uri,
                  'remote_ip': handler.request.remote_ip,
                  'status': handler.get_status(),
                  'request_time_ms': handler.request.request_time() * 1000.0,
                  'service_id': self.service_id,
                  'request_id': handler.request.headers.get(REQUEST_ID_HEADER,
                                                            'undefined')
                  }

        # handler can optionally define additional data to log
        if hasattr(handler, 'logvalues'):
            for key, value in handler.logvalues.iteritems():
                packet[key] = value

        servicelog.log(packet)

        metric = "requests." + str(handler.get_status())
        metrics.timing(metric, handler.request.request_time() * 1000.0)

        super(LoggingApplication, self).log_request(handler)


class RequestHandler(tornado.web.RequestHandler):
    """A handler with helpers"""

    def appurl(self):
        """Return URL for app"""
        return self.request.protocol + "://" + self.request.host

    def caller(self):
        """Returns caller's ID (from X-Auth-User)"""
        return self.request.headers.get(AUTH_USER_HEADER, 'undefined')

    def logvalue(self, key, value):
        """Add log entry to request log info"""
        if not hasattr(self, 'logvalues'):
            self.logvalues = {}
        self.logvalues[key] = value

    def halt(self, code, msg=None):
        """Halt processing. Raise HTTP error with given code and message."""
        raise tornado.web.HTTPError(code, msg)

    def json(self, obj):
        """Use our own encoder to support additional types"""
        return json.dumps(obj, cls=JSONEncoder)

    def write_error(self, status_code, **kwargs):
        """Log halt_reason in service log and output error page"""
        message = default_message = httplib.responses.get(status_code, '')
        # HTTPError exceptions may have a log_message attribute
        if 'exc_info' in kwargs:
            (_, exc, _) = kwargs['exc_info']
            if hasattr(exc, 'log_message'):
                message = str(exc.log_message) or default_message
        self.logvalue('halt_reason', message)
        title = "{}: {}".format(status_code, default_message)
        body = "{}: {}".format(status_code, message)
        self.finish("<html><title>" + title + "</title>"
                    "<body>" + body + "</body></html>")

    def timeit(self, metric, func, *args, **kwargs):
        """Time execution of callable and emit metric then return result."""
        return metrics.timeit(metric, func, *args, **kwargs)

    def require_json_content_type(self):
        """Raise 400 error if content type is not json"""
        self.require_content_type('application/json')

    def require_content_type(self, content_type):
        """Raises a 400 if request content type is not as specified."""
        if self.request.headers.get('content-type', '') != content_type:
            self.halt(400, 'Content type must be ' + content_type)

    def prepare(self):
        """Override base method to add a request ID header if needed"""
        self._ensure_request_id_header()

    @property
    def request_id(self):
        """Return request ID from header"""
        return self.request.headers.get(REQUEST_ID_HEADER, 'undefined')

    def set_headers(self, headers):
        """Set headers"""
        for (header, value) in headers.iteritems():
            self.set_header(header, value)

    def _ensure_request_id_header(self):
        "Ensure request headers have a request ID. Set one if needed."
        if REQUEST_ID_HEADER not in self.request.headers:
            self.request.headers.add(REQUEST_ID_HEADER, uuid.uuid1().hex)



class JSONEncoder(json.JSONEncoder):
    """
    Adds support for datetime.datetime objects.

    >>> json.dumps({'a': datetime.datetime(2013, 12, 10)}, cls=JSONEncoder)
    '{"a": "2013-12-10T00:00:00Z"}'
    >>> json.dumps({'a': datetime.time()}, cls=JSONEncoder)
    Traceback (most recent call last):
        ...
    TypeError: datetime.time(0, 0) is not JSON serializable
    """
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.strftime("%Y-%m-%dT%H:%M:%SZ")
        else:
            return json.JSONEncoder.default(self, obj)



"""
Tornado server utilities

- LoggingApplication: a base Application class with logging and metrics
- RequestHandler: a base request handler with helpers

Dependencies:
- metrics
- servicelog

See tests/test_tornadoutil.py for usage example.

"""

import json
import datetime
import uuid
import httplib # for httplib.responses

import tornado.web
import tornado.options
import tornado.httpserver
import metrics
import servicelog


REQUEST_ID_HEADER = 'X-Request-Id'
AUTH_USER_HEADER = 'X-Auth-User'


class LoggingApplication(tornado.web.Application):
    """
    Overrides base log_request method to log requests to JSON
    UDP collector. Logs method, uri, remote_ip, etc.

    Also supports logging arbitrary key/value pair logging via a handler's
    'logvalues' attribute.

    >>> app = LoggingApplication('myservice')
    """
    def __init__(self, service_id, *args, **kwargs):
        self.service_id = service_id or 'undefined'
        metrics.configure(prefix=self.service_id)
        super(LoggingApplication, self).__init__(*args, **kwargs)

    def run(self, port): # pragma: no coverage
        """
        Run on given port. Parse standard options and start the http server.
        """
        tornado.options.parse_command_line()
        http_server = tornado.httpserver.HTTPServer(self)
        http_server.listen(port)
        tornado.ioloop.IOLoop.instance().start()

    def log_request(self, handler):
        """
        Override base method to log requests to JSON UDP collector and emit
        a metric.
        """
        packet = {'method': handler.request.method,
                  'uri': handler.request.uri,
                  'remote_ip': handler.request.remote_ip,
                  'status': handler.get_status(),
                  'request_time_ms': handler.request.request_time() * 1000.0,
                  'service_id': self.service_id,
                  'request_id': handler.request.headers.get(REQUEST_ID_HEADER,
                                                            'undefined')
                  }

        # handler can optionally define additional data to log
        if hasattr(handler, 'logvalues'):
            for key, value in handler.logvalues.iteritems():
                packet[key] = value

        servicelog.log(packet)

        metric = "requests." + str(handler.get_status())
        metrics.timing(metric, handler.request.request_time() * 1000.0)

        super(LoggingApplication, self).log_request(handler)


class RequestHandler(tornado.web.RequestHandler):
    """A handler with helpers"""

    def appurl(self):
        """Return URL for app"""
        return self.request.protocol + "://" + self.request.host

    def caller(self):
        """Returns caller's ID (from X-Auth-User)"""
        return self.request.headers.get(AUTH_USER_HEADER, 'undefined')

    def logvalue(self, key, value):
        """Add log entry to request log info"""
        if not hasattr(self, 'logvalues'):
            self.logvalues = {}
        self.logvalues[key] = value

    def halt(self, code, msg=None):
        """Halt processing. Raise HTTP error with given code and message."""
        raise tornado.web.HTTPError(code, msg)

    def json(self, obj):
        """Use our own encoder to support additional types"""
        return json.dumps(obj, cls=JSONEncoder)

    def write_error(self, status_code, **kwargs):
        """Log halt_reason in service log and output error page"""
        message = default_message = httplib.responses.get(status_code, '')
        # HTTPError exceptions may have a log_message attribute
        if 'exc_info' in kwargs:
            (_, exc, _) = kwargs['exc_info']
            if hasattr(exc, 'log_message'):
                message = str(exc.log_message) or default_message
        self.logvalue('halt_reason', message)
        title = "{}: {}".format(status_code, default_message)
        body = "{}: {}".format(status_code, message)
        self.finish("<html><title>" + title + "</title>"
                    "<body>" + body + "</body></html>")

    def timeit(self, metric, func, *args, **kwargs):
        """Time execution of callable and emit metric then return result."""
        return metrics.timeit(metric, func, *args, **kwargs)

    def require_json_content_type(self):
        """Raise 400 error if content type is not json"""
        self.require_content_type('application/json')

    def require_content_type(self, content_type):
        """Raises a 400 if request content type is not as specified."""
        if self.request.headers.get('content-type', '') != content_type:
            self.halt(400, 'Content type must be ' + content_type)

    def prepare(self):
        """Override base method to add a request ID header if needed"""
        self._ensure_request_id_header()

    @property
    def request_id(self):
        """Return request ID from header"""
        return self.request.headers.get(REQUEST_ID_HEADER, 'undefined')

    def set_headers(self, headers):
        """Set headers"""
        for (header, value) in headers.iteritems():
            self.set_header(header, value)

    def _ensure_request_id_header(self):
        "Ensure request headers have a request ID. Set one if needed."
        if REQUEST_ID_HEADER not in self.request.headers:
            self.request.headers.add(REQUEST_ID_HEADER, uuid.uuid1().hex)



class JSONEncoder(json.JSONEncoder):
    """
    Adds support for datetime.datetime objects.

    >>> json.dumps({'a': datetime.datetime(2013, 12, 10)}, cls=JSONEncoder)
    '{"a": "2013-12-10T00:00:00Z"}'
    >>> json.dumps({'a': datetime.time()}, cls=JSONEncoder)
    Traceback (most recent call last):
        ...
    TypeError: datetime.time(0, 0) is not JSON serializable
    """
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.strftime("%Y-%m-%dT%H:%M:%SZ")
        else:
            return json.JSONEncoder.default(self, obj)



Tornado server utilities  - LoggingApplication: a base Application class with logging and metrics - RequestHandler: a base request handler with helpers  Dependencies: - metrics - servicelog  See tests/test_tornadoutil.py for usage example. # for httplib.responses Overrides base log_request method to log requests to JSON     UDP collector. Logs method, uri, remote_ip, etc.      Also supports logging arbitrary key/value pair logging via a handler's     'logvalues' attribute.      >>> app = LoggingApplication('myservice') # pragma: no coverage Run on given port. Parse standard options and start the http server. Override base method to log requests to JSON UDP collector and emit         a metric. # handler can optionally define additional data to log A handler with helpers Return URL for app Returns caller's ID (from X-Auth-User) Add log entry to request log info Halt processing. Raise HTTP error with given code and message. Use our own encoder to support additional types Log halt_reason in service log and output error page # HTTPError exceptions may have a log_message attribute Time execution of callable and emit metric then return result. Raise 400 error if content type is not json Raises a 400 if request content type is not as specified. Override base method to add a request ID header if needed Return request ID from header Set headers Adds support for datetime.datetime objects.      >>> json.dumps({'a': datetime.datetime(2013, 12, 10)}, cls=JSONEncoder)     '{"a": "2013-12-10T00:00:00Z"}'     >>> json.dumps({'a': datetime.time()}, cls=JSONEncoder)     Traceback (most recent call last):         ...     TypeError: datetime.time(0, 0) is not JSON serializable