-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds a monitoring dashboard to Gradio apps that can be used to view usage #8478
Changes from 10 commits
fce24e0
714f4d4
2557df9
7227985
646899f
3e59ea8
32f6489
bdf5080
cf5e7da
46dc7c0
8f7c1ea
6bd5dae
186c644
c03269a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"gradio": minor | ||
--- | ||
|
||
feat:Adds a monitoring dashboard to Gradio apps that can be used to view usage |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import random | ||
import time | ||
|
||
import pandas as pd | ||
|
||
import gradio as gr | ||
|
||
data = {"data": {}} | ||
|
||
with gr.Blocks() as demo: | ||
with gr.Row(): | ||
selected_function = gr.Dropdown( | ||
["All"], | ||
value="All", | ||
label="Endpoint", | ||
info="Select the function to see analytics for, or 'All' for aggregate.", | ||
scale=2, | ||
) | ||
demo.load( | ||
lambda: gr.Dropdown( | ||
choices=["All"] | ||
+ list({row["function"] for row in data["data"].values()}) # type: ignore | ||
), | ||
None, | ||
selected_function, | ||
) | ||
timespan = gr.Dropdown( | ||
["All Time", "24 hours", "1 hours", "10 minutes"], | ||
value="All Time", | ||
label="Timespan", | ||
info="Duration to see data for.", | ||
) | ||
with gr.Group(): | ||
with gr.Row(): | ||
unique_users = gr.Label(label="Unique Users") | ||
unique_requests = gr.Label(label="Unique Requests") | ||
process_time = gr.Label(label="Avg Process Time") | ||
plot = gr.BarPlot( | ||
x="time", | ||
y="count", | ||
color="status", | ||
title="Requests over Time", | ||
y_title="Requests", | ||
width=600, | ||
) | ||
|
||
@gr.on( | ||
[demo.load, selected_function.change, timespan.change], | ||
inputs=[selected_function, timespan], | ||
outputs=[unique_users, unique_requests, process_time, plot], | ||
) | ||
def load_dfs(function, timespan): | ||
df = pd.DataFrame(data["data"].values()) | ||
if df.empty: | ||
return 0, 0, 0, gr.skip() | ||
df["time"] = pd.to_datetime(df["time"], unit="s") | ||
df_filtered = df if function == "All" else df[df["function"] == function] | ||
if timespan != "All Time": | ||
df_filtered = df_filtered[ | ||
df_filtered["time"] > pd.Timestamp.now() - pd.Timedelta(timespan) | ||
] | ||
|
||
df_filtered["time"] = df_filtered["time"].dt.floor("T") | ||
plot = df_filtered.groupby(["time", "status"]).size().reset_index(name="count") # type: ignore | ||
mean_process_time_for_success = df_filtered[df_filtered["status"] == "success"][ | ||
"process_time" | ||
].mean() | ||
|
||
return ( | ||
df_filtered["status"].nunique(), | ||
df_filtered.shape[0], | ||
round(mean_process_time_for_success, 2), | ||
plot, | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
data["data"] = { | ||
random.randint(0, 1000000): { | ||
"time": time.time() - random.randint(0, 60 * 60 * 24 * 3), | ||
"status": random.choice( | ||
["success", "success", "failure", "pending", "queued"] | ||
), | ||
"function": random.choice(["predict", "chat", "chat"]), | ||
"process_time": random.randint(0, 10), | ||
} | ||
for r in range(random.randint(100, 200)) | ||
} | ||
|
||
demo.launch() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -164,6 +164,8 @@ def __init__( | |
): | ||
self.tokens = {} | ||
self.auth = None | ||
self.analytics_key = secrets.token_urlsafe(16) | ||
self.analytics_enabled = False | ||
self.blocks: gradio.Blocks | None = None | ||
self.state_holder = StateHolder() | ||
self.iterators: dict[str, AsyncIterator] = {} | ||
|
@@ -1165,6 +1167,32 @@ def robots_txt(): | |
else: | ||
return "User-agent: *\nDisallow: " | ||
|
||
@app.get("/monitoring") | ||
async def analytics_login(): | ||
print( | ||
f"Monitoring URL: {app.get_blocks().local_url}monitoring/{app.analytics_key}" | ||
) | ||
return HTMLResponse("See console for monitoring URL.") | ||
|
||
@app.get("/monitoring/{key}") | ||
async def analytics_dashboard(key: str): | ||
if key == app.analytics_key: | ||
analytics_url = f"/monitoring/{app.analytics_key}/dashboard" | ||
if not app.analytics_enabled: | ||
from gradio.analytics_dashboard import data | ||
from gradio.analytics_dashboard import demo as dashboard | ||
|
||
mount_gradio_app(app, dashboard, path=analytics_url) | ||
dashboard._queue.start() | ||
analytics = app.get_blocks()._queue.event_analytics | ||
data["data"] = analytics | ||
app.analytics_enabled = True | ||
return RedirectResponse( | ||
url=analytics_url, status_code=status.HTTP_302_FOUND | ||
Comment on lines
+1177
to
+1191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understand correctly, this will start monitoring only after the first time a user visits this endpoint? Is that intentional - why do we want to do that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fwiw this essentially allows anyone to "start the monitoring" on anyone else's Gradio app There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anyways, if we do do this, how about we also offer the ability for users to set a parameter in Its also easier to describe and publicize -- get an API dashboard with just a single parameter! I.e.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, only starts the dashboard app when visiting the secret monitoring link, can add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Nvm I misunderstood, this is fine There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe the parameter is still useful as a way to display the URL right there at launch but not a big deal either way |
||
) | ||
else: | ||
raise HTTPException(status_code=403, detail="Invalid key.") | ||
|
||
return app | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this real data or mock data?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its mock data that's created if you run this file directly, but will use real data in gradio runtime
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool