-
Notifications
You must be signed in to change notification settings - Fork 126
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
prometheus extra #213
Merged
arnaudmiribel
merged 9 commits into
arnaudmiribel:main
from
sfc-gh-jcarroll:prometheus-registry
Feb 2, 2024
Merged
prometheus extra #213
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
cf51cea
Initial setup for streamlit_prometheus extra
sfc-gh-jcarroll 4b461b3
Cleanup and simplification!
sfc-gh-jcarroll 8c1a857
Better docstring
sfc-gh-jcarroll 7f70f59
Rename to reduce stutter
sfc-gh-jcarroll 3671666
Add link to fuller example
sfc-gh-jcarroll 42346b2
PR feedback
sfc-gh-jcarroll 8eade10
Update lock file to see if it resolves dependency issue
sfc-gh-jcarroll ae57588
Add example repo in-place and update link
sfc-gh-jcarroll f845f0d
Prometheus: Add note about using Streamlit 1.31+
sfc-gh-jcarroll File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
from typing import List, NamedTuple | ||
|
||
from prometheus_client import CollectorRegistry | ||
from prometheus_client.openmetrics.exposition import generate_latest | ||
from streamlit.runtime.stats import CacheStatsProvider | ||
|
||
from .. import extra | ||
|
||
|
||
class CustomStat(NamedTuple): | ||
metric_str: str = "" | ||
|
||
def to_metric_str(self) -> str: | ||
return self.metric_str | ||
|
||
def marshall_metric_proto(self, metric) -> None: | ||
"""Custom OpenMetrics collected via protobuf is not currently supported.""" | ||
# Included to be compatible with the RequestHandler's _stats_to_proto() method: | ||
# https://github.com/streamlit/streamlit/blob/1.29.0/lib/streamlit/web/server/stats_request_handler.py#L73 | ||
# Fill in dummy values so protobuf format isn't broken | ||
label = metric.labels.add() | ||
label.name = "cache_type" | ||
label.value = "custom_metrics" | ||
|
||
label = metric.labels.add() | ||
label.name = "cache" | ||
label.value = "not_implemented" | ||
|
||
metric_point = metric.metric_points.add() | ||
metric_point.gauge_value.int_value = 0 | ||
|
||
|
||
class PrometheusMetricsProvider(CacheStatsProvider): | ||
def __init__(self, registry: CollectorRegistry): | ||
self.registry = registry | ||
|
||
def get_stats(self) -> List[CustomStat]: | ||
sfc-gh-jcarroll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Use generate_latest() method provided by prometheus to produce the | ||
appropriately formatted OpenMetrics text encoding for all the stored metrics. | ||
|
||
Then do a bit of string manipulation to package it in the format expected | ||
by Streamlit's stats handler, so the final output looks the way we expect. | ||
""" | ||
DUPLICATE_SUFFIX = "\n# EOF\n" | ||
output_str = generate_latest(self.registry).decode(encoding="utf-8") | ||
if not output_str.endswith(DUPLICATE_SUFFIX): | ||
raise ValueError("Unexpected output from OpenMetrics text encoding") | ||
output = CustomStat(metric_str=output_str[: -len(DUPLICATE_SUFFIX)]) | ||
return [output] | ||
|
||
|
||
@extra | ||
def streamlit_registry() -> CollectorRegistry: | ||
""" | ||
Expose Prometheus metrics (https://prometheus.io) from your Streamlit app. | ||
|
||
Create and use Prometheus metrics in your app with `registry=streamlit_registry()`. | ||
The metrics will be exposed at Streamlit's existing `/_stcore/metrics` route. | ||
|
||
**Note:** This extra works best with Streamlit >= 1.31. There are known bugs with | ||
some earlier Streamlit versions, especially 1.30.0. | ||
|
||
See more example metrics in the Prometheus Python docs: | ||
https://prometheus.github.io/client_python/ | ||
|
||
To produce accurate metrics, you are responsible to ensure that unique metric | ||
objects are shared across app runs and sessions. We recommend either 1) initialize | ||
metrics in a separate file and import them in the main app script, or 2) initialize | ||
metrics in a cached function (and ensure the cache is not cleared during execution). | ||
|
||
For an app running locally you can view the output with | ||
`curl localhost:8501/_stcore/metrics` or equivalent. | ||
sfc-gh-jcarroll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
from streamlit import runtime | ||
|
||
stats = runtime.get_instance().stats_mgr | ||
|
||
# Did we already register it elsewhere? If so, return that copy | ||
for prv in stats._cache_stats_providers: | ||
if isinstance(prv, PrometheusMetricsProvider): | ||
return prv.registry | ||
|
||
# This is the function was called, so create the registry | ||
# and hook it into Streamlit stats | ||
registry = CollectorRegistry(auto_describe=True) | ||
prv = PrometheusMetricsProvider(registry=registry) | ||
stats.register_provider(prv) | ||
return registry | ||
|
||
|
||
def example(): | ||
import streamlit as st | ||
from prometheus_client import Counter | ||
|
||
@st.cache_resource | ||
def get_metric(): | ||
registry = streamlit_registry() | ||
return Counter( | ||
name="my_counter", | ||
documentation="A cool counter", | ||
labelnames=("app_name",), | ||
registry=registry, # important!! | ||
) | ||
|
||
SLIDER_COUNT = get_metric() | ||
|
||
app_name = st.text_input("App name", "prometheus_app") | ||
latest = st.slider("Latest value", 0, 20, 3) | ||
if st.button("Submit"): | ||
SLIDER_COUNT.labels(app_name).inc(latest) | ||
|
||
st.write( | ||
""" | ||
View a fuller example that uses the (safer) import metrics method at: | ||
https://github.com/arnaudmiribel/streamlit-extras/tree/main/src/streamlit_extras/prometheus/example | ||
""" | ||
) | ||
|
||
st.write( | ||
""" | ||
### Example output at `{host:port}/_stcore/metrics` | ||
``` | ||
# TYPE my_counter counter | ||
# HELP my_counter A cool counter | ||
my_counter_total{app_name="prometheus_app"} 14.0 | ||
my_counter_created{app_name="prometheus_app"} 1.7042185907557938e+09 | ||
``` | ||
""" | ||
) | ||
|
||
|
||
__title__ = "Prometheus" | ||
__desc__ = "Expose Prometheus metrics (https://prometheus.io) from your Streamlit app." | ||
__icon__ = "📊" | ||
__examples__ = [example] | ||
__author__ = "Joshua Carroll" | ||
__experimental_playground__ = False | ||
__stlite__ = False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# Streamlit Prometheus example | ||
|
||
This repo has a simple example of a Streamlit app that exposes prometheus metrics via `streamlit_extras.prometheus`. | ||
|
||
It demonstrates the approach of creating metrics in a separate file and importing them into your main app file | ||
for object persistence across runs. | ||
|
||
## Running the example | ||
|
||
```sh | ||
pip install streamlit-extras | ||
streamlit run app.py | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import time | ||
from random import random | ||
|
||
import streamlit as st | ||
from metrics import EXEC_TIME, SLIDER_COUNT | ||
|
||
st.header("Streamlit app with Prometheus metrics") | ||
|
||
""" | ||
1. Enter an app name and slider value and press Submit | ||
1. At a terminal, do `curl localhost:8501/_stcore/metrics` to view the metrics generated | ||
1. Note you can run this across multiple sessions and it aggregates the counter | ||
""" | ||
|
||
start_time = time.time() | ||
|
||
|
||
app_name = st.text_input("App name", "prometheus_app") | ||
latest = st.slider("Latest value", 0, 20, 3) | ||
if st.button("Submit"): | ||
SLIDER_COUNT.labels(app_name).inc(latest) | ||
st.toast("Successfully submitted") | ||
|
||
# Add a little variability to the response | ||
time.sleep(random() / 2) | ||
exec_time = time.time() - start_time | ||
st.write(f"Exec time was {exec_time}") | ||
EXEC_TIME.labels("main").observe(exec_time) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from prometheus_client import Counter, Histogram | ||
from streamlit_extras.prometheus import streamlit_registry | ||
|
||
registry = streamlit_registry() | ||
|
||
SLIDER_COUNT = Counter( | ||
name="slider_count", | ||
documentation="Total submitted count of the slider", | ||
labelnames=("app_name",), | ||
registry=registry, | ||
) | ||
|
||
EXEC_TIME = Histogram( | ||
name="page_exec_seconds", | ||
documentation="Execution time of each page run", | ||
labelnames=("page",), | ||
registry=registry, | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 import something that's been valid for a few releases of streamlit already? We don't pin streamlit to anything else then > 1.0.0 so if this was introduced only in 1.31.0, that may break for others. LMK
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.
(I read
**Note:** This extra works best with Streamlit >= 1.31. There are known bugs with some earlier Streamlit versions, especially 1.30.0.
, just curious if it would bug or break with prior)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.
Yes, this import has worked since 1.12.
There was a bug in Streamlit's native implementation of Prometheus since before 1.12 -> 1.30 where it printed duplicate metric rows that breaks the protocol format. I think in practice most collectors would be able to handle it but it might throw a warning or something (and we never surfaced the endpoint for real use so no one complained). So this collector would work prior to 1.30 but the Streamlit portion might be slightly weird (I think there's a hacky way to fix it if anyone complains about that, I could do another PR).
Karen fixed that issue in 1.30 streamlit/streamlit#7921 in a way that totally broke this prometheus collector. So the only version that won't work at all is 1.30. Then he fixed it to be compatible with this AND have the bug fix in 1.31.
I think it's OK with the note but LMK if you think we should handle another way!
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.
Thanks a lot for those precisions. I was mostly being careful for ImportErrors, but 1.12 is already a while back!
Let's do it!