PK!.®timing_asgi/__init__.pyfrom .middleware import TimingMiddleware from .interfaces import TimingClient, MetricNamer __version__ = "0.1.0rc1" __all__ = [MetricNamer, TimingClient, TimingMiddleware] PK!iq)NN$timing_asgi/integrations/__init__.pyfrom .starlette import StarletteScopeToName __all__ = [StarletteScopeToName] PK!s܉OO%timing_asgi/integrations/starlette.pyfrom starlette.routing import Match from timing_asgi.interfaces import MetricNamer from timing_asgi.utils import PathToName class StarletteScopeToName(MetricNamer): """ Class which extracts a suitable metric name from a matching Starlette route. Uses the route name if it has one, but otherwise constructs a name based on the full module + method/class path. Falls back to either a provided fallback callable, or uses PathToName, which is the TimingMiddleware default. """ def __init__(self, prefix, starlette_app, fallback=None): self.prefix = prefix self.starlette_app = starlette_app if fallback is None: fallback = PathToName(prefix) self.fallback = fallback def __call__(self, scope): route = None for r in self.starlette_app.router.routes: if r.matches(scope)[0] == Match.FULL: route = r break if route is not None: return f"{self.prefix}.{route.endpoint.__module__}.{route.name}" else: return self.fallback(scope) PK!+ڈtiming_asgi/interfaces.pyfrom abc import ABC, abstractmethod from typing import List class MetricNamer(ABC): @abstractmethod def __call__(self, scope) -> str: pass # pragma: no cover class TimingClient(ABC): """ An abstract class detailing the client interface """ @abstractmethod def timing(self, metric_name: str, timing: float, tags: List[str]): pass # pragma: no cover PK!h4 4 timing_asgi/middleware.pyimport alog import functools from .utils import PathToName, TimingStats class TimingMiddleware: """ Timing middleware for ASGI applications Args: app (ASGI application): ASGI application client (TimingClient): the client used to emit instrumentation metrics metric_namer (MetricNamer): the callable used to construct metric names from the ASGI scope """ def __init__(self, app, client, metric_namer=None): if metric_namer is None: metric_namer = PathToName(prefix="unnamed") self.app = app self.client = self.ensure_compliance(client) self.metric_namer = metric_namer def ensure_compliance(self, client): assert hasattr(client, 'timing') assert callable(client.timing) return client def __call__(self, scope): return functools.partial(self.asgi, asgi_scope=scope) async def asgi(self, receive, send, asgi_scope): app = self.app(asgi_scope) # locals inside the app function (send_wrapper) can't be assigned to, # as the interpreter detects the assignment and thus creates a new # local variable within that function, with that name. instance = {'http_status_code': None} def send_wrapper(response): if response['type'] == 'http.response.start': instance['http_status_code'] = response['status'] return send(response) if asgi_scope['type'] != 'http': alog.info(f"ASGI scope of type {asgi_scope['type']} is not supported yet") await app(receive, send) return try: metric_name = self.metric_namer(asgi_scope) except AttributeError as e: alog.error(f"Unable to extract metric name from asgi scope: {asgi_scope}, skipping statsd timing") alog.error(f" -> exception: {e}") await app(receive, send) return def emit(stats): statsd_tags = [ f"http_status:{instance['http_status_code']}", f"http_method:{asgi_scope['method']}" ] self.client.timing(f"{metric_name}", stats.time, tags=statsd_tags + ["time:wall"]) self.client.timing(f"{metric_name}", stats.cpu_time, tags=statsd_tags + ["time:cpu"]) with TimingStats(metric_name) as stats: try: await app(receive, send_wrapper) except Exception: stats.stop() instance['http_status_code'] = 500 emit(stats) raise emit(stats) PK!566timing_asgi/utils.pyimport resource import time from .interfaces import MetricNamer def get_cpu_time(): resources = resource.getrusage(resource.RUSAGE_SELF) # add up user time (ru_utime) and system time (ru_stime) return resources[0] + resources[1] class TimingStats(object): def __init__(self, name=None): self.name = name def start(self): self.start_time = time.time() self.start_cpu_time = get_cpu_time() def stop(self): self.end_time = time.time() self.end_cpu_time = get_cpu_time() def __enter__(self): self.start() return self @property def time(self): return self.end_time - self.start_time @property def cpu_time(self): return self.end_cpu_time - self.start_cpu_time def __exit__(self, exc_type, exc_value, traceback): self.stop() class PathToName(MetricNamer): def __init__(self, prefix): self.prefix = prefix def __call__(self, scope): path = scope.get('path')[1:] return f"{self.prefix}.{path.replace('/', '.')}" PK!H$TT!timing_asgi-0.1.2.dist-info/WHEEL = 0 нR \C HCoo~ \B"|lchлYh|hzWٓ7}|v }PK!HŖ0%$timing_asgi-0.1.2.dist-info/METADATAQIN1})cMDD "s+8 c;"C>cn]U].yJKݓ쬂lRٰw˶숢6Qq9eY=At@ e ડ+~&c(Ub5x7fbK!q异"[ U} ֵ~DJˍ©n\:#nfO![%e1te?80~dJ)hP FwT68=gϲN{4_}'hu4f9DX9 A.Odꧏe~(>PK!Hl"timing_asgi-0.1.2.dist-info/RECORD}r0{,z؃(A( B' ~ɖ=CI%x/$j<&11Y3b