Skip to content

Commit

Permalink
Add basic HTTP metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
Klavionik committed Dec 25, 2023
1 parent 5b6772c commit da59e2d
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 6 deletions.
25 changes: 23 additions & 2 deletions battleship/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
PlayerSubscriptionHandler,
SessionSubscriptionHandler,
)
from battleship.server.metrics import (
MetricsMiddleware,
MetricsScraperAuthenticationHandler,
)
from battleship.server.pubsub import (
IncomingChannel,
IncomingRedisChannel,
Expand Down Expand Up @@ -82,7 +86,11 @@ async def sentry_context_middleware(

if identity and identity.is_authenticated():
sentry_sdk.set_user(
{"id": identity.sub, "username": identity["nickname"], "email": identity["email"]}
{
"id": identity.sub,
"username": identity.get("nickname"),
"email": identity.get("email"),
}
)

return await handler(request)
Expand Down Expand Up @@ -115,6 +123,10 @@ def create_app() -> Any:
)
)

app.use_authentication().add(
MetricsScraperAuthenticationHandler(scraper_secret=config.METRICS_SCRAPER_SECRET)
)

app.use_authorization().with_default_policy(
Policy("authenticated", AuthenticatedRequirement()),
)
Expand All @@ -124,7 +136,16 @@ def create_app() -> Any:

app.middlewares.append(client_version_middleware)
app.middlewares.append(sentry_context_middleware)
app_router = app.router

if config.SENTRY_DSN:
return configure_sentry(app, config.SENTRY_DSN, config.SERVER_VERSION)
app = configure_sentry(
app,
config.SENTRY_DSN,
config.SERVER_VERSION,
) # type: ignore[assignment]

if config.METRICS_SCRAPER_SECRET:
app = MetricsMiddleware(app, router=app_router) # type: ignore[assignment]

return app
1 change: 1 addition & 0 deletions battleship/server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Config(BaseSettings):
BROKER_URL: RedisDsn
SERVER_VERSION: str
SENTRY_DSN: str
METRICS_SCRAPER_SECRET: str

@property
def auth0_audience(self) -> str:
Expand Down
72 changes: 72 additions & 0 deletions battleship/server/metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from typing import Any

from aioprometheus.asgi.middleware import MetricsMiddleware as _MetricsMiddleware
from aioprometheus.asgi.middleware import Receive, Scope, Send
from aioprometheus.collectors import REGISTRY
from aioprometheus.renderer import render
from blacksheep import Request, Router
from guardpost import AuthenticationHandler, Identity

__all__ = [
"MetricsMiddleware",
"MetricsScraperAuthenticationHandler",
"render_metrics",
]


class MetricsScraperAuthenticationHandler(AuthenticationHandler):
def __init__(self, scraper_secret: str):
self._secret = scraper_secret.encode()

def authenticate(self, context: Request) -> Identity | None:
header_value = context.get_first_header(b"Authorization")

try:
type_, secret = header_value.split() # type: ignore[union-attr]
except Exception:
context.identity = None
else:
if type_ == b"Bearer" and secret == self._secret:
context.identity = Identity({"id": "scraper"}, authentication_mode="Bearer")
else:
context.identity = None

return context.identity


class MetricsMiddleware(_MetricsMiddleware):
"""
ioprometheus.MetricsMiddleware that doesn't fail on WebSocket connections
and extracts template paths from Blacksheep.
"""

def __init__(self, *args: Any, router: Router, **kwargs: Any):
super().__init__(*args, **kwargs)
self.router = router

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "websocket":
await self.asgi_callable(scope, receive, send)
return

await super().__call__(scope, receive, send)

def get_full_or_template_path(self, scope: Scope) -> str:
root_path = scope.get("root_path", "")
path = scope.get("path", "")
full_path = f"{root_path}{path}"
method = scope.get("method", "").upper()

if self.use_template_urls:
match = self.router.get_match_by_method_and_path(method, path)

if match is not None:
return match.pattern.decode()
return full_path


def render_metrics(accept_headers: list[bytes]) -> tuple[str, dict[bytes, bytes]]:
accept_headers_decoded = [value.decode() for value in accept_headers]
content, headers = render(REGISTRY, accept_headers_decoded)
headers = {k.encode(): v.encode() for k, v in headers.items()}
return content.decode(), headers
13 changes: 12 additions & 1 deletion battleship/server/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from blacksheep import (
FromJSON,
Request,
Response,
Router,
WebSocket,
Expand All @@ -22,6 +23,7 @@
PlayerSubscriptionHandler,
SessionSubscriptionHandler,
)
from battleship.server.metrics import render_metrics
from battleship.server.pubsub import IncomingChannel, OutgoingChannel
from battleship.server.repositories import (
ClientRepository,
Expand All @@ -41,7 +43,7 @@
SignupCredentials,
)

router = Router() # type: ignore[no-untyped-call]
router = Router()


@router.ws("/ws")
Expand Down Expand Up @@ -231,3 +233,12 @@ async def get_player_statistics(
statistics = await statistics_repository.create(user_id)

return ok(statistics.to_dict())


@router.get("/metrics")
async def get_metrics(request: Request) -> Response:
accept_headers = request.get_headers(b"Accept")
content, headers = render_metrics(accept_headers)
response = ok(content)
response.headers.add_many(headers)
return response
95 changes: 93 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ textual-dev = { version = "^1.2.1", optional = true }
async-timeout = { version = "^4.0.3", optional = true, markers = "python_version < '3.11'" }
backports-strenum = { version = "^1.2.8", markers = "python_version < '3.11'" }
sentry-sdk = { version = "^1.38.0", optional = true }
aioprometheus = {version = "^23.3.0", optional = true}


[tool.poetry.extras]
server = ["blacksheep", "uvicorn", "auth0-python", "pyjwt", "uvloop", "redis", "sentry-sdk"]
server = ["blacksheep", "uvicorn", "auth0-python", "pyjwt", "uvloop", "redis", "sentry-sdk", "aioprometheus"]
client = ["textual", "typer", "pyee", "async-timeout"]
dev = ["textual-dev"]

Expand Down
3 changes: 3 additions & 0 deletions requirements-server.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
aiohttp==3.9.1 ; python_version >= "3.10" and python_version < "4.0"
aioprometheus==23.3.0 ; python_version >= "3.10" and python_version < "4.0"
aiosignal==1.3.1 ; python_version >= "3.10" and python_version < "4.0"
annotated-types==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
anyio==4.1.0 ; python_version >= "3.10" and python_version < "4.0"
Expand Down Expand Up @@ -30,6 +31,7 @@ itsdangerous==2.1.2 ; python_version >= "3.10" and python_version < "4.0"
loguru==0.7.2 ; python_version >= "3.10" and python_version < "4.0"
markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0"
multidict==6.0.4 ; python_version >= "3.10" and python_version < "4.0"
orjson==3.9.10 ; python_version >= "3.10" and python_version < "4.0"
pycparser==2.21 ; python_version >= "3.10" and python_version < "4.0"
pydantic-core==2.14.5 ; python_version >= "3.10" and python_version < "4.0"
pydantic-settings==2.1.0 ; python_version >= "3.10" and python_version < "4.0"
Expand All @@ -40,6 +42,7 @@ pyopenssl==23.3.0 ; python_version >= "3.10" and python_version < "4.0"
python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0"
python-dotenv==1.0.0 ; python_version >= "3.10" and python_version < "4.0"
pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
quantile-python==1.1 ; python_version >= "3.10" and python_version < "4.0"
redis==5.0.1 ; python_version >= "3.10" and python_version < "4.0"
requests==2.31.0 ; python_version >= "3.10" and python_version < "4.0"
rodi==2.0.6 ; python_version >= "3.10" and python_version < "4.0"
Expand Down

0 comments on commit da59e2d

Please sign in to comment.