diff --git a/setup.py b/setup.py index 7c856d84..4232eed7 100644 --- a/setup.py +++ b/setup.py @@ -83,6 +83,7 @@ 'http_sfv>=0.9.8', 'dataclasses_json>=0.5.7', 'apispec>=6.3.0', + 'prometheus-client>=0.17.1' ], extras_require={ # eg: diff --git a/src/keria/app/agenting.py b/src/keria/app/agenting.py index 14874089..26c5658c 100644 --- a/src/keria/app/agenting.py +++ b/src/keria/app/agenting.py @@ -43,6 +43,8 @@ from ..core.keeping import RemoteManager from ..db import basing +from .metrics import MetricsMiddleware, MetricsEndpoint + logger = ogler.getLogger() @@ -68,7 +70,18 @@ def setup(name, bran, adminPort, bootPort, base='', httpPort=None, configFile=No 'signify-resource', 'signify-timestamp'])) if os.getenv("KERI_AGENT_CORS", "false").lower() in ("true", "1"): app.add_middleware(middleware=httping.HandleCORS()) - app.add_middleware(authing.SignatureValidationComponent(agency=agency, authn=authn, allowed=["/agent"])) + allowed_routes = ["/agent"] + if os.getenv("KERI_AGENT_METRICS", "false").lower() in ("true", "1"): + promethus = MetricsMiddleware() + bootApp.add_middleware(promethus) + MetricsEnds = MetricsEndpoint(promethus.registry) + bootApp.add_route("/metrics", MetricsEnds) + + app.add_middleware(promethus) + app.add_route("/metrics", promethus) + allowed_routes.append("/metrics") + + app.add_middleware(authing.SignatureValidationComponent(agency=agency, authn=authn, allowed=allowed_routes)) app.req_options.media_handlers.update(media.Handlers()) app.resp_options.media_handlers.update(media.Handlers()) @@ -82,6 +95,10 @@ def setup(name, bran, adminPort, bootPort, base='', httpPort=None, configFile=No presenting.loadEnds(app=app) notifying.loadEnds(app=app) + + + + if httpPort: happ = falcon.App(middleware=falcon.CORSMiddleware( allow_origins='*', allow_credentials='*', diff --git a/src/keria/app/metrics.py b/src/keria/app/metrics.py new file mode 100644 index 00000000..e085761c --- /dev/null +++ b/src/keria/app/metrics.py @@ -0,0 +1,41 @@ +from prometheus_client import CollectorRegistry, Counter, Histogram, generate_latest +import time + +# Define metrics +REQUESTS = Counter('requests_total', 'Total Request Count', ['method', 'path', 'resource']) +ERRORS = Counter('errors_total', 'Total Error Count', ['method', 'path', 'resource', 'status']) +DURATION = Histogram('request_duration_seconds', 'Duration of requests', ['method', 'path', 'resource']) + +class MetricsMiddleware: + def __init__(self): + """Initialize the middleware with Prometheus metrics.""" + self.registry = CollectorRegistry() + self.registry.register(REQUESTS) + self.registry.register(ERRORS) + self.registry.register(DURATION) + def process_request(self, req, resp): + """Process the request before routing it.""" + req.context['resource'] = req.get_header('signify-resource') + req.context['start_time'] = time.time() + + def process_resource(self, req, resp, resource, params): + """Process the request after routing.""" + REQUESTS.labels(method=req.method, path=req.path, resource=req.context['resource']).inc() + + def process_response(self, req, resp, resource, req_succeeded): + """Post-processing of the response (after routing).""" + if 'start_time' in req.context: + duration = time.time() - req.context['start_time'] + DURATION.labels(method=req.method, path=req.path, resource=req.context.get('resource', '')).observe(duration) + + if not req_succeeded: + ERRORS.labels(method=req.method, path=req.path, resource=req.context.get('resource', ''), status=resp.status).inc() + +class MetricsEndpoint: + def __init__(self, registry): + self.registry = registry + + def on_get(self, req, resp): + resp.content_type = 'text/plain; version=0.0.4; charset=utf-8' + data = generate_latest(self.registry) + resp.body = str(data.decode('utf-8'))