From 597ed93023f08859e8e32476823ae05a042a6dbc Mon Sep 17 00:00:00 2001 From: Roger Yang Date: Tue, 19 Mar 2024 15:37:17 -0700 Subject: [PATCH] chore: add prometheus --- Dockerfile | 13 +++--- pyproject.toml | 3 ++ src/phoenix/server/app.py | 8 ++++ src/phoenix/server/main.py | 6 +++ src/phoenix/server/prometheus.py | 76 ++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 src/phoenix/server/prometheus.py diff --git a/Dockerfile b/Dockerfile index 8278d059f2..d4e9e00c63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # This dockerfile is provided for convenience if you wish to run -# Phoenix in a docker container / sidecar. +# Phoenix in a docker container / sidecar. # To use this dockerfile, you must first build the phoenix image # using the following command: # > docker build -t phoenix . @@ -23,18 +23,21 @@ ADD . /phoenix # Install the app by building the typescript package -RUN cd /phoenix/app && npm install && npm run build && rm -rf /phoenix/app +RUN cd /phoenix/app && npm install && npm run build && rm -rf /phoenix/app FROM builder # delete symbolic links RUN find . -xtype l -delete -# Install any needed packages -RUN pip install . +# Install any needed packages +RUN pip install .[prometheus] # Make port 6006 available to the world outside this container EXPOSE 6006 +# Prometheus +EXPOSE 9090 + # Run server.py when the container launches -CMD ["python", "src/phoenix/server/main.py", "--host", "0.0.0.0", "--port", "6006", "serve"] \ No newline at end of file +CMD ["python", "src/phoenix/server/main.py", "--host", "0.0.0.0", "--port", "6006", "--enable-prometheus", "True", "serve"] diff --git a/pyproject.toml b/pyproject.toml index 76154e0d31..96cc694973 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,9 @@ llama-index = [ "llama-index-callbacks-arize-phoenix>=0.1.2", "openinference-instrumentation-llama-index>=1.2.0", ] +prometheus = [ + "prometheus-client", +] [project.urls] Documentation = "https://docs.arize.com/phoenix/" diff --git a/src/phoenix/server/app.py b/src/phoenix/server/app.py index 5b31132038..60e812e374 100644 --- a/src/phoenix/server/app.py +++ b/src/phoenix/server/app.py @@ -151,6 +151,7 @@ def create_app( span_store: Optional[SpanStore] = None, debug: bool = False, read_only: bool = False, + enable_prometheus: bool = False, ) -> Starlette: graphql = GraphQLWithContext( schema=schema, @@ -160,9 +161,16 @@ def create_app( export_path=export_path, graphiql=True, ) + if enable_prometheus: + from phoenix.server.prometheus import PrometheusMiddleware + + prometheus_middlewares = [Middleware(PrometheusMiddleware)] + else: + prometheus_middlewares = [] return Starlette( middleware=[ Middleware(HeadersMiddleware), + *prometheus_middlewares, ], debug=debug, routes=( diff --git a/src/phoenix/server/main.py b/src/phoenix/server/main.py index 13cb9a9399..070a745f46 100644 --- a/src/phoenix/server/main.py +++ b/src/phoenix/server/main.py @@ -129,6 +129,7 @@ def _load_items( parser.add_argument("--no-internet", action="store_true") parser.add_argument("--umap_params", type=str, required=False, default=DEFAULT_UMAP_PARAMS_STR) parser.add_argument("--debug", action="store_false") + parser.add_argument("--enable-prometheus", type=bool, default=False) subparsers = parser.add_subparsers(dest="command", required=True) serve_parser = subparsers.add_parser("serve") datasets_parser = subparsers.add_parser("datasets") @@ -223,6 +224,10 @@ def _load_items( ) read_only = args.read_only logger.info(f"Server umap params: {umap_params}") + if enable_prometheus := args.enable_prometheus: + from phoenix.server.prometheus import start_prometheus + + start_prometheus() app = create_app( export_path=export_path, model=model, @@ -232,6 +237,7 @@ def _load_items( debug=args.debug, read_only=read_only, span_store=span_store, + enable_prometheus=enable_prometheus, ) host = args.host or get_env_host() port = args.port or get_env_port() diff --git a/src/phoenix/server/prometheus.py b/src/phoenix/server/prometheus.py new file mode 100644 index 0000000000..b1038105d5 --- /dev/null +++ b/src/phoenix/server/prometheus.py @@ -0,0 +1,76 @@ +import time +from threading import Thread + +import psutil +from prometheus_client import ( + Counter, + Gauge, + Histogram, + start_http_server, +) +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Match + +REQUESTS_PROCESSING_TIME = Histogram( + name="starlette_requests_processing_time_seconds", + documentation="Histogram of requests processing time by method and path (in seconds)", + labelnames=["method", "path"], +) +EXCEPTIONS = Counter( + name="starlette_exceptions_total", + documentation="Total count of exceptions raised by method, path and exception type", + labelnames=["method", "path", "exception_type"], +) +RAM_METRIC = Gauge( + name="memory_usage_bytes", + documentation="Memory usage in bytes", + labelnames=["type"], +) +CPU_METRIC = Gauge( + name="cpu_usage_percent", + documentation="CPU usage percent", + labelnames=["core"], +) + + +class PrometheusMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + for route in request.app.routes: + match, _ = route.matches(request.scope) + if match is Match.FULL: + path = route.path + break + else: + return await call_next(request) + method = request.method + start_time = time.perf_counter() + try: + response = await call_next(request) + except BaseException as e: + EXCEPTIONS.labels(method=method, path=path, exception_type=type(e).__name__).inc() + raise + stop_time = time.perf_counter() + REQUESTS_PROCESSING_TIME.labels(method=method, path=path).observe(stop_time - start_time) + return response + + +def start_prometheus() -> None: + Thread(target=gather_system_data, daemon=True).start() + start_http_server(9090) + + +def gather_system_data() -> None: + while True: + time.sleep(1) + + ram = psutil.virtual_memory() + swap = psutil.swap_memory() + + RAM_METRIC.labels(type="virtual").set(ram.used) + RAM_METRIC.labels(type="swap").set(swap.used) + + # Add cpu metrics + for c, p in enumerate(psutil.cpu_percent(interval=1, percpu=True)): + CPU_METRIC.labels(core=c).set(p)