|
#!/usr/bin/env python |
|
|
|
"""This is our complete Tornado application. |
|
|
|
Example endpoints... |
|
|
|
curl localhost:9000/headers |
|
curl localhost:9000/static/foo.js |
|
curl localhost:9000/ |
|
""" |
|
|
|
import logging |
|
import os |
|
import structlog |
|
|
|
# from tornado import gen |
|
from tornado.httpclient import AsyncHTTPClient |
|
from tornado.httpserver import HTTPServer |
|
from tornado.ioloop import IOLoop |
|
from tornado.options import options |
|
from tornado.process import cpu_count |
|
from tornado.web import Application, RequestHandler, StaticFileHandler |
|
from tornado.web import ErrorHandler as BaseErrorHandler |
|
|
|
app_settings = { |
|
"compress_response": True, |
|
"default_handler_args": dict(status_code=404), |
|
"default_handler_class": ErrorHandler, |
|
"env": os.environ.get("ENV", "dev"), |
|
"log_level": os.environ.get("LOG_LEVEL", "INFO"), |
|
"port": os.environ.get("APP_PORT", "9000"), |
|
"static_path": os.path.join(os.path.dirname(__file__), "static"), |
|
"static_handler_class": RobotsHandler, # probably should be more generically named |
|
"template_path": os.path.join(os.path.dirname(__file__), "templates"), |
|
# debug=True (can also use explicit: tornado.autoreload.start()) |
|
# |
|
# Example: |
|
# |
|
# if settings.get("cluster") == "dev": |
|
# tornado.autoreload.start() |
|
} |
|
|
|
logging.basicConfig( |
|
level=getattr(logging, app_settings["log_level"]), |
|
format="[%(levelname)s %(asctime)s path:%(pathname)s lineno:%(lineno)s] %(message)s", # noqa |
|
datefmt="%Y/%m/%d %I:%M:%S" |
|
) |
|
|
|
structlog.configure(logger_factory=structlog.stdlib.LoggerFactory()) |
|
log = structlog.get_logger() |
|
|
|
|
|
class HeadersHandler(RequestHandler): |
|
"""Serves application header information.""" |
|
|
|
# Following method is here to demonstrate older Tornado syntax |
|
# Pre-async/await support version, where you needed Tornado's gen module |
|
# |
|
# @gen.coroutine |
|
# def get(self, *args, **kwargs): |
|
# """A function that will return the content for the health endpoint.""" # noqa |
|
# log.info("Endpoint hit", handler="health") |
|
# http_client = AsyncHTTPClient() |
|
# response = yield http_client.fetch("https://httpbin.org/headers") |
|
# self.finish(response.body) |
|
|
|
def initialize(self, some_data_object): |
|
self.some_data_object = some_data_object |
|
|
|
async def get(self, *args, **kwargs): |
|
"""A function that will return the content for the header endpoint.""" |
|
log.info("Endpoint hit", handler="health") |
|
http_client = AsyncHTTPClient() |
|
response = await http_client.fetch("https://httpbin.org/headers") |
|
self.finish(response.body) |
|
|
|
def data_received(self, chunk): |
|
"""Defined to avoid abstract-method lint issue.""" |
|
pass |
|
|
|
|
|
class HomeHandler(RequestHandler): |
|
"""Serves application home information.""" |
|
|
|
async def get(self, *args, **kwargs): |
|
"""A function that will return the content for the home endpoint.""" |
|
log.info("Endpoint hit", handler="home") |
|
self.render( |
|
"index.html", |
|
header_text="Header goes here", |
|
footer_text="Footer goes here" |
|
) |
|
|
|
def data_received(self, chunk): |
|
"""Defined to avoid abstract-method lint issue.""" |
|
pass |
|
|
|
|
|
class RobotsHandler(StaticFileHandler): |
|
async def get(self, path="robots.txt", include_body=True): |
|
return await super().get(path, include_body) |
|
|
|
def set_default_headers(self): |
|
self.clear_header("Server") |
|
|
|
def set_extra_headers(self, path): |
|
self.set_header("foo", "bar") |
|
|
|
|
|
class ErrorHandler(BaseErrorHandler): |
|
def write_error(self, status_code, **kwargs): |
|
log = logger.bind(status=status_code) |
|
log.error("incoming request") |
|
|
|
# notice these methods... |
|
self.set_default_headers() |
|
self.set_extra_headers() |
|
# ...are no longer automatically called |
|
# |
|
# so I manually call them |
|
# |
|
# i believe this is because tornado.web.ErrorHandler doesn't call the parent RequestHandler __init__ |
|
|
|
self.write("Error %s" % status_code) |
|
|
|
def set_default_headers(self): |
|
self.clear_header("Server") |
|
|
|
def set_extra_headers(self, path=None): |
|
for header, value in get_headers(self.get_status(), key=path): |
|
self.set_header(header, value) |
|
|
|
|
|
def robots_path() -> str: |
|
return os.path.join(os.path.dirname(__file__), "static", "robots.txt") |
|
|
|
|
|
class App(Application): |
|
"""Base Application class to define the app's routes and settings.""" |
|
|
|
def __init__(self): |
|
"""Setup the routes and import the application settings.""" |
|
app_handlers = [ |
|
(r"/", HomeHandler), |
|
(r"/headers", HeadersHandler, {"some_data_object": ["i", "could", "be", "a", "class"]}), |
|
(r"/robots.txt", RobotsHandler, {"path": robots_path()}), # MUST set `static_handler_class` in app_settings |
|
] |
|
|
|
super().__init__(app_handlers, **app_settings) |
|
|
|
|
|
if __name__ == "__main__": |
|
options.logging = None # configure tornado to leave logging alone |
|
log = log.bind(port=app_settings["port"]) |
|
|
|
if app_settings["env"] == "dev": |
|
log.info("Starting applications", mode="single") |
|
App().listen(app_settings["port"]) |
|
else: |
|
log.info("Starting applications", mode="forked", cpu_count=cpu_count()) |
|
server = HTTPServer(App()) |
|
server.bind(app_settings["port"]) |
|
server.start(0) # multi process mode (one process per cpu) |
|
|
|
IOLoop.current().start() |