Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added examples/images/tornado.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions examples/tornado/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Tornado example
This example demonstrates the integration with the [Tornado](https://www.tornadoweb.org/).

## Running the example
1. Start an instance of SBA (Spring Boot Admin):
```sh
docker run --rm -p 8080:8080 michayaak/spring-boot-admin:2.2.3-1
```
2. Once Spring Boot Admin is running, you can run the examples as follow:
```sh
cd examples/tornado
poetry install
poetry run python -m tornado_example_app
```

![tornado Example](../images/tornado.png)

18 changes: 18 additions & 0 deletions examples/tornado/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[tool.poetry]
name = "tornado-pyctuator-example"
version = "1.0.0"
description = "Example of using Pyctuator"
authors = [
"Desmond Stonie <aneasystone@gmail.com>",
]

[tool.poetry.dependencies]
python = "^3.7"
psutil = { version = "^5.6" }
tornado = { version = "^6.0.4" }
pyctuator = { version = "^0.13" }

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

37 changes: 37 additions & 0 deletions examples/tornado/tornado_example_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import datetime
import logging
import random

from tornado import ioloop
from tornado.web import Application, RequestHandler
from tornado.httpserver import HTTPServer

from pyctuator.pyctuator import Pyctuator

my_logger = logging.getLogger("example")

class HomeHandler(RequestHandler):
def get(self):
my_logger.debug(f"{datetime.datetime.now()} - {str(random.randint(0, 100))}")
print("Printing to STDOUT")
self.write("Hello World!")

app = Application([
(r"/", HomeHandler)
], debug=False)

example_app_address = "host.docker.internal"
example_sba_address = "localhost"

Pyctuator(
app,
"Tornado Pyctuator",
app_url=f"http://{example_app_address}:5000",
pyctuator_endpoint_url=f"http://{example_app_address}:5000/pyctuator",
registration_url=f"http://{example_sba_address}:8080/instances",
app_description="Demonstrate Spring Boot Admin Integration with Tornado",
)

http_server = HTTPServer(app, decompress_request=True)
http_server.listen(5000)
ioloop.IOLoop.current().start()
113 changes: 113 additions & 0 deletions pyctuator/impl/tornado_pyctuator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import dataclasses
import json
from collections import defaultdict
from datetime import datetime
from functools import partial
from typing import Any, Callable, List, Mapping

from tornado.web import Application, RequestHandler

from pyctuator.httptrace import TraceRecord, TraceRequest, TraceResponse
from pyctuator.impl import SBA_V2_CONTENT_TYPE
from pyctuator.impl.pyctuator_impl import PyctuatorImpl
from pyctuator.impl.pyctuator_router import PyctuatorRouter

class AbstractPyctuatorHandler(RequestHandler):
pyctuator = None
dumps = None
def initialize(self):
self.pyctuator = self.application.settings.get('pyctuator')
self.dumps = self.application.settings.get('custom_dumps')

class PyctuatorHandler(AbstractPyctuatorHandler):
def get(self):
resp = self.pyctuator.get_endpoints_data()
self.write(self.dumps(resp))

# GET /env
class EnvHandler(AbstractPyctuatorHandler):
def options(self):
self.write('')
def get(self):
resp = self.pyctuator.pyctuator_impl.get_environment()
self.write(self.dumps(resp))

# GET /info
class InfoHandler(AbstractPyctuatorHandler):
def options(self):
self.write('')
def get(self):
resp = self.pyctuator.pyctuator_impl.app_info
self.write(self.dumps(resp))

# GET /health
class HealthHandler(AbstractPyctuatorHandler):
def options(self):
self.write('')
def get(self):
resp = self.pyctuator.pyctuator_impl.get_health()
self.write(self.dumps(resp))

# GET /metrics
class MetricsHandler(AbstractPyctuatorHandler):
def options(self):
self.write('')
def get(self):
resp = self.pyctuator.pyctuator_impl.get_metric_names()
self.write(self.dumps(resp))

# GET "/metrics/{metric_name}"
class MetricsNameHandler(AbstractPyctuatorHandler):
def get(self, metric_name):
resp = self.pyctuator.pyctuator_impl.get_metric_measurement(metric_name)
self.write(self.dumps(resp))

# GET /loggers
class LoggersHandler(AbstractPyctuatorHandler):
def options(self):
self.write('')
def get(self):
resp = self.pyctuator.pyctuator_impl.logging.get_loggers()
self.write(self.dumps(resp))

# GET /loggers/{logger_name}
# POST /loggers/{logger_name}
class LoggersNameHandler(AbstractPyctuatorHandler):
def get(self, logger_name):
resp = self.pyctuator.pyctuator_impl.logging.get_logger(logger_name)
self.write(self.dumps(resp))
def post(self, logger_name):
body = self.request.body.decode('utf-8')
body = json.loads(body)
self.pyctuator.pyctuator_impl.logging.set_logger_level(logger_name, body.get('configuredLevel', None))
self.write('')

# pylint: disable=too-many-locals,unused-argument
class TornadoHttpPyctuator(PyctuatorRouter):
def __init__(self, app: Application, pyctuator_impl: PyctuatorImpl) -> None:
super().__init__(app, pyctuator_impl)

custom_dumps = partial(
json.dumps, default=self._custom_json_serializer
)

app.settings.setdefault("pyctuator", self)
app.settings.setdefault("custom_dumps", custom_dumps)
app.add_handlers(".*$", [
(r"/pyctuator", PyctuatorHandler),
(r"/pyctuator/env", EnvHandler),
(r"/pyctuator/info", InfoHandler),
(r"/pyctuator/health", HealthHandler),
(r"/pyctuator/metrics", MetricsHandler),
(r"/pyctuator/metrics/(?P<metric_name>.*$)", MetricsNameHandler),
(r"/pyctuator/loggers", LoggersHandler),
(r"/pyctuator/loggers/(?P<logger_name>.*$)", LoggersNameHandler),
])

def _custom_json_serializer(self, value: Any) -> Any:
if dataclasses.is_dataclass(value):
return dataclasses.asdict(value)

if isinstance(value, datetime):
return str(value)
return None
16 changes: 16 additions & 0 deletions pyctuator/pyctuator.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def __init__(

* aiohttp - `app` is an instance of `aiohttp.web.Application`

* Tornado - `app` is an instance of `tornado.web.Application`

:param app: an instance of a supported web-framework with which the pyctuator endpoints will be registered
:param app_name: the application's name that will be presented in the "Info" section in boot-admin
:param app_description: a description that will be presented in the "Info" section in boot-admin
Expand Down Expand Up @@ -101,6 +103,7 @@ def __init__(
"flask": self._integrate_flask,
"fastapi": self._integrate_fastapi,
"aiohttp": self._integrate_aiohttp,
"tornado": self._integrate_tornado
}
for framework_name, framework_integration_function in framework_integrations.items():
if self._is_framework_installed(framework_name):
Expand Down Expand Up @@ -195,3 +198,16 @@ def _integrate_aiohttp(self, app: Any, pyctuator_impl: PyctuatorImpl) -> bool:
AioHttpPyctuator(app, pyctuator_impl)
return True
return False

def _integrate_tornado(self, app: Any, pyctuator_impl: PyctuatorImpl) -> bool:
"""
This method should only be called if we detected that tornado is installed.
It will then check whether the given app is a tornado app, and if so - it will add the Pyctuator
endpoints to it.
"""
from tornado.web import Application
if isinstance(app, Application):
from pyctuator.impl.tornado_pyctuator import TornadoHttpPyctuator
TornadoHttpPyctuator(app, pyctuator_impl)
return True
return False
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ PyMySQL = {version = "^0.9.3", optional = true}
cryptography = {version = "^2.8", optional = true}
redis = {version = "^3.3", optional = true}
aiohttp = {version = "^3.6.2", optional = true}
tornado = {version = "^6.0.4", optional = true}

[tool.poetry.dev-dependencies]
requests = "^2.22"
Expand All @@ -56,6 +57,7 @@ psutil = ["psutil"]
fastapi = ["fastapi", "uvicorn"]
flask = ["flask"]
aiohttp = ["aiohttp"]
tornado = ["tornado"]
db = ["sqlalchemy", "PyMySQL", "cryptography"]
redis = ["redis"]

Expand Down