From d3c56497f388434b92fa423c86eb740aa4199fd2 Mon Sep 17 00:00:00 2001 From: Brett Beutell Date: Wed, 5 Apr 2023 15:04:16 +0200 Subject: [PATCH 1/7] Update README and start adding documentation to the examples --- README.md | 13 ++++++---- examples/README.md | 49 ++++++++++++++++++++++++++++++++++++++ examples/caller-example.py | 1 + examples/docs-example.py | 5 +++- examples/example.py | 27 +++++++++++++-------- 5 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 examples/README.md diff --git a/README.md b/README.md index 3a797d1..4d5ad42 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,12 @@ A Python decorator that makes it easy to understand the error rate, response time, and production usage of any function in your code. Jump straight from your IDE to live Prometheus charts for each HTTP/RPC handler, database method, or other piece of application logic. -Autometrics for Python provides a decorator that can create [Prometheus](https://prometheus.io/) metrics for your functions and class methods throughout your code base, as well as a function that will write corresponding Prometheus queries for you in a Markdown file. +Autometrics for Python provides: -[See Why Autometrics?](https://github.com/autometrics-dev#why-autometrics) for more details on the ideas behind autometrics +1. A decorator that can create [Prometheus](https://prometheus.io/) metrics for your functions and class methods throughout your code base. +2. A helper function that will write corresponding Prometheus queries for you in a Markdown file. + +See [Why Autometrics?](https://github.com/autometrics-dev#why-autometrics) for more details on the ideas behind autometrics. ## Features @@ -35,9 +38,11 @@ def sayHello: ``` -- If you like to access the queries for your decoraded functions you can run `help(yourfunction)` or `print(yourfunction.__doc__)` +- To access the PromQL queries for your decorated functions, run `help(yourfunction)` or `print(yourfunction.__doc__)`. + +- To show tooltips over decorated functions in VSCode, with links to Prometheus queries, try installing [the VSCode extension](https://marketplace.visualstudio.com/items?itemName=Fiberplane.autometrics). -- Unfortunately it is not possible to have the queries in the tooltips due to the [static Analyzer](https://github.com/davidhalter/jedi/issues/1921). We are currently figuring out to build a VS Code PlugIn to make it work. +> Note that we cannot support tooltips without a VSCode extension due to behavior of the [static analyzer](https://github.com/davidhalter/jedi/issues/1921) used in VSCode. ## Development of the package diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..330ae44 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,49 @@ +# autometrics-py examples + +You should be able to run each example by running `python examples/.py` from the root of the repo. + +You can change the base url for Prometheus links via the `PROMETHEUS_URL` environment variable. For example: + +```sh +PROMETHEUS_URL=http://localhost:9091/ python examples/example.py +``` + +Read more below about each example, and what kind of features they demonstrate. + +## `docs-example.py` + +This script shows how the autometrics decorator augments the docstring for a python function. + +We simply decorate a function, then print its docstring to the console using the built-in `help` function. + +## `example.py` + +This script demonstrates the basic usage of the `autometrics` decorator. When you run `python examples/example.py`, it will output links to metrics in your configured prometheus instance. You can change the base url for prometheus links via the `PROMETHEUS_URL` environment variable + +```sh +PROMETHEUS_URL=http://localhost:9091/ python examples/example.py +``` + +This Python script defines a class called "Operations" that has too methods: "add", "div_handled". These methods perform addition and division operations, respectively, and include code to handle potential errors. + +There is also a top-level function named "div_unhandled", which performs division without handling any errors. + +To test out autometrics, we start an HTTP server on port 8080 using the Prometheus client library, which exposes our metrics to prometheus (via a `/metrics` endpoint). + +Then, we enter an infinite loop (with a 2 second sleep period), calling the "div_handled", "add", and "div_unhandled" methods repeatedly with different input parameters. + +This should start generating data that you can explore in prometheus. Just follow the links that are printed to the console! + +Don't forget to configure prometheus to scrape the metrics endpoint! + +```yaml +# Example prometheus.yaml +scrape_configs: + - job_name: "python-autometrics-example" + metrics_path: /metrics + static_configs: + - targets: ["localhost:8080"] + # For a real deployment, you would want the scrape interval to be + # longer but for testing, you want the data to show up quickly + scrape_interval: 500ms +``` diff --git a/examples/caller-example.py b/examples/caller-example.py index df69c0d..16d5f9a 100644 --- a/examples/caller-example.py +++ b/examples/caller-example.py @@ -1,5 +1,6 @@ print("hello") from prometheus_client import start_http_server +from autometrics.autometrics import autometrics @autometrics diff --git a/examples/docs-example.py b/examples/docs-example.py index a0f3f57..3e78bdf 100644 --- a/examples/docs-example.py +++ b/examples/docs-example.py @@ -1,4 +1,3 @@ -import time from autometrics.autometrics import autometrics @@ -8,4 +7,8 @@ def hello(): print("Hello") +# Use the built-in `help` function to print the docstring for `hello` +# +# In your console, you'll see links to prometheus metrics for the `hello` function, +# which were added by the `autometrics` decorator. help(hello) diff --git a/examples/example.py b/examples/example.py index fd73351..86530aa 100644 --- a/examples/example.py +++ b/examples/example.py @@ -1,9 +1,13 @@ from prometheus_client import start_http_server from autometrics.autometrics import autometrics import time +import random - -class Operations: +# Defines a class called `Operations`` that has two methods: +# 1. `add` - Perform addition +# 2. `div_handled` - Perform division and handle errors +# +class Operations(): def __init__(self, **args): self.args = args @@ -23,30 +27,33 @@ def div_handled(self, num1, num2): result = e.__class__.__name__ return result - +# Perform division without handling errors @autometrics def div_unhandled(num1, num2): result = num1 / num2 return result -@autometrics -def text_print(): - return "hello" - - ops = Operations() +# Show the docstring (with links to prometheus metrics) for the `add` method print(ops.add.__doc__) + +# Show the docstring (with links to prometheus metrics) for the `div_unhandled` method print(div_unhandled.__doc__) +# Start an HTTP server on port 8080 using the Prometheus client library, which exposes our metrics to prometheus start_http_server(8080) + +# Enter an infinite loop (with a 2 second sleep period), calling the "div_handled", "add", and "div_unhandled" methods, +# in order to generate metrics. while True: ops.div_handled(2, 0) ops.add(1, 2) ops.div_handled(2, 1) - div_unhandled(2, 0) - text_print() + # Randomly call `div_unhandled` with a 50/50 chance of raising an error + div_unhandled(2, random.randint(0, 1)) ops.add(1, 2) time.sleep(2) + # Call `div_unhandled` such that it raises an error div_unhandled(2, 0) From fc8baa22516d24f390a6d134beec2dae2aefa9df Mon Sep 17 00:00:00 2001 From: Brett Beutell Date: Wed, 5 Apr 2023 15:38:34 +0200 Subject: [PATCH 2/7] Modify caller example --- examples/caller-example.py | 44 ++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/examples/caller-example.py b/examples/caller-example.py index 16d5f9a..4e8720b 100644 --- a/examples/caller-example.py +++ b/examples/caller-example.py @@ -1,20 +1,46 @@ -print("hello") from prometheus_client import start_http_server from autometrics.autometrics import autometrics +import time +import random - +# This is moana, who would rather explore the ocean than prometheus metrics @autometrics -def message(): - return "hello" +def moana(): + return "surf's up!" +# This is neo, the one (that we'll end up calling) +@autometrics +def neo(): + return "i know kung fu" +# This is simba. Rawr. @autometrics -def greet(name): - m = message() - greeting = f"hello {name}, {m}" - return greeting +def simba(): + return "rawr" +# Define a function that randomly calls `moana`, `neo`, or `simba` +@autometrics +def destiny(): + random_int = random.randint(0, 2) + if random_int == 0: + return f"Destiny is calling moana. moana says: {moana()}" + elif random_int == 1: + return f"Destiny is calling neo. neo says: {neo()}" + else: + return f"Destiny is calling simba. simba says: {simba()}" + +# Start an HTTP server on port 8080 using the Prometheus client library, which exposes our metrics to prometheus start_http_server(8080) + +print(f"Try this PromQL query in your Prometheus dashboard:\n") +print(f"# Rate of calls to the `destiny` function per second, averaged over 5 minute windows\n") +print('sum by (function, module) (rate(function_calls_count_total{caller="destiny"}[5m]))') + +# Enter an infinite loop (with a 1 second sleep period), calling the `destiny` and `agent_smith` methods. while True: - greet("john") + destiny() + time.sleep(0.3) + + +# NOTE - You will want to open prometheus From 6b441dcf18fe9f514dfcc0816514a8b637f81bc8 Mon Sep 17 00:00:00 2001 From: Brett Beutell Date: Wed, 5 Apr 2023 15:44:17 +0200 Subject: [PATCH 3/7] Update examples README --- examples/README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/README.md b/examples/README.md index 330ae44..11f0dc4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,8 +1,8 @@ # autometrics-py examples -You should be able to run each example by running `python examples/.py` from the root of the repo. +You should be able to run each example by executing `python examples/.py` from the root of the repo. -You can change the base url for Prometheus links via the `PROMETHEUS_URL` environment variable. For example: +You can change the base url for Prometheus links via the `PROMETHEUS_URL` environment variable. So, if your local Prometheus were on a non-default port, like 9091, you would run: ```sh PROMETHEUS_URL=http://localhost:9091/ python examples/example.py @@ -18,23 +18,15 @@ We simply decorate a function, then print its docstring to the console using the ## `example.py` -This script demonstrates the basic usage of the `autometrics` decorator. When you run `python examples/example.py`, it will output links to metrics in your configured prometheus instance. You can change the base url for prometheus links via the `PROMETHEUS_URL` environment variable +This script demonstrates the basic usage of the `autometrics` decorator. When you run `python examples/example.py`, it will output links to metrics in your configured prometheus instance. -```sh -PROMETHEUS_URL=http://localhost:9091/ python examples/example.py -``` - -This Python script defines a class called "Operations" that has too methods: "add", "div_handled". These methods perform addition and division operations, respectively, and include code to handle potential errors. - -There is also a top-level function named "div_unhandled", which performs division without handling any errors. +You can read the script for comments on how it works, but the basic idea is that we have a division function (`div_unhandled`) that occasionally divides by zero and does not catch its errors. We can see its error rate in prometheus via the links in its doc string. -To test out autometrics, we start an HTTP server on port 8080 using the Prometheus client library, which exposes our metrics to prometheus (via a `/metrics` endpoint). +Note that the script starts an HTTP server on port 8080 using the Prometheus client library, which exposes metrics to prometheus (via a `/metrics` endpoint). -Then, we enter an infinite loop (with a 2 second sleep period), calling the "div_handled", "add", and "div_unhandled" methods repeatedly with different input parameters. +Then, it enters into an infinite loop (with a 2 second sleep period), calling methods repeatedly with different input parameters. This should start generating data that you can explore in Prometheus. Just follow the links that are printed to the console! -This should start generating data that you can explore in prometheus. Just follow the links that are printed to the console! - -Don't forget to configure prometheus to scrape the metrics endpoint! +Don't forget to configure Prometheus to scrape the metrics endpoint. Here's an example `prometheus.yaml` file: ```yaml # Example prometheus.yaml @@ -47,3 +39,11 @@ scrape_configs: # longer but for testing, you want the data to show up quickly scrape_interval: 500ms ``` + +## `caller-example.py` + +Autometrics also tracks a label, `caller`, which is the name of the function that called the decorated function. The `caller-example.py` script shows how to use that label. It uses the same structure as the `example.py` script, but it prints a PromQL query that you can use to explore the caller data yourself. + +## `fastapi-example.py` + +> TODO From f548801184cf6b07a7dda3294342e215f84331e7 Mon Sep 17 00:00:00 2001 From: Brett Beutell Date: Wed, 5 Apr 2023 16:57:16 +0200 Subject: [PATCH 4/7] Document the fastapi example --- examples/README.md | 32 ++++++++++++++++++++++++++++++-- examples/fastapi-example.py | 4 +++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/examples/README.md b/examples/README.md index 11f0dc4..51f35ab 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,7 +26,7 @@ Note that the script starts an HTTP server on port 8080 using the Prometheus cli Then, it enters into an infinite loop (with a 2 second sleep period), calling methods repeatedly with different input parameters. This should start generating data that you can explore in Prometheus. Just follow the links that are printed to the console! -Don't forget to configure Prometheus to scrape the metrics endpoint. Here's an example `prometheus.yaml` file: +Don't forget to configure Prometheus itself to scrape the metrics endpoint. Here's an example `prometheus.yaml` file: ```yaml # Example prometheus.yaml @@ -44,6 +44,34 @@ scrape_configs: Autometrics also tracks a label, `caller`, which is the name of the function that called the decorated function. The `caller-example.py` script shows how to use that label. It uses the same structure as the `example.py` script, but it prints a PromQL query that you can use to explore the caller data yourself. +Don't forget to configure Prometheus itself to scrape the metrics endpoint. Here's an example `prometheus.yaml` file: + +```yaml +# Example prometheus.yaml +scrape_configs: + - job_name: "python-autometrics-example" + metrics_path: /metrics + static_configs: + - targets: ["localhost:8080"] + # For a real deployment, you would want the scrape interval to be + # longer but for testing, you want the data to show up quickly + scrape_interval: 500ms +``` + ## `fastapi-example.py` -> TODO +This is an example that shows you how to use autometrics to get metrics on http handlers with FastAPI. In this case, we're setting up the API ourselves, which means we need to expose a `/metrics` endpoint manually. + +Don't forget to configure Prometheus itself to scrape the metrics endpoint. Here's an example `prometheus.yaml` file: + +```yaml +# Example prometheus.yaml +scrape_configs: + - job_name: "python-autometrics-example" + metrics_path: /metrics + static_configs: + - targets: ["localhost:8080"] + # For a real deployment, you would want the scrape interval to be + # longer but for testing, you want the data to show up quickly + scrape_interval: 500ms +``` diff --git a/examples/fastapi-example.py b/examples/fastapi-example.py index 49d6cc8..9fe5964 100644 --- a/examples/fastapi-example.py +++ b/examples/fastapi-example.py @@ -5,12 +5,14 @@ app = FastAPI() - +# Set up a metrics endpoint for Prometheus to scrape +# `generate_lates` returns the latest metrics data in the Prometheus text format @app.get("/metrics") def metrics(): return Response(generate_latest()) +# Set up the root endpoint of the API @app.get("/") @autometrics def read_root(): From 21dfb8000ff30c9ba33e93c01a52a725458912b1 Mon Sep 17 00:00:00 2001 From: Brett Beutell Date: Wed, 5 Apr 2023 17:02:36 +0200 Subject: [PATCH 5/7] Update code example in root README --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4d5ad42..101da63 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,14 @@ See [Why Autometrics?](https://github.com/autometrics-dev#why-autometrics) for m ## Using autometrics-py -- Requirement: a running [prometheus instance](https://prometheus.io/download/) -- include a .env file with your prometheus endpoint `PROMETHEUS_URL = your endpoint`, if not defined the default endpoint will be `http://localhost:9090/` +- Set up a [Prometheus instance](https://prometheus.io/download/) +- Configure prometheus to scrape your application ([check our instructions if you need help](https://github.com/autometrics-dev#5-configuring-prometheus)) +- Include a .env file with your prometheus endpoint `PROMETHEUS_URL=your endpoint`. If this is not defined, the default endpoint will be `http://localhost:9090/` - `pip install autometrics` - Import the library in your code and use the decorator for any function: -``` -from autometrics import autometrics +```py +from autometrics.autometrics import autometrics @autometrics def sayHello: From ff0b691b99ff8c9e042aabc45b07b26235f2abbd Mon Sep 17 00:00:00 2001 From: Brett Beutell Date: Wed, 5 Apr 2023 17:18:48 +0200 Subject: [PATCH 6/7] Refer to the prometheus.yaml file for how to set up scraping with examples --- examples/README.md | 56 ++++++++++++++-------------------------------- prometheus.yaml | 13 ++++++----- 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/examples/README.md b/examples/README.md index 51f35ab..8b8c264 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,6 +10,20 @@ PROMETHEUS_URL=http://localhost:9091/ python examples/example.py Read more below about each example, and what kind of features they demonstrate. +Also, for the examples that expose a `/metrics` endpoint, you will need to configure Prometheus to scrape that endpoint. There is an example `prometheus.yaml` file in the root of this project, but here is the relevant part: + +```yaml +# Example prometheus.yaml +scrape_configs: + - job_name: "python-autometrics-example" + metrics_path: /metrics + static_configs: + - targets: ["localhost:8080"] + # For a real deployment, you would want the scrape interval to be + # longer but for testing, you want the data to show up quickly + scrape_interval: 500ms +``` + ## `docs-example.py` This script shows how the autometrics decorator augments the docstring for a python function. @@ -26,52 +40,16 @@ Note that the script starts an HTTP server on port 8080 using the Prometheus cli Then, it enters into an infinite loop (with a 2 second sleep period), calling methods repeatedly with different input parameters. This should start generating data that you can explore in Prometheus. Just follow the links that are printed to the console! -Don't forget to configure Prometheus itself to scrape the metrics endpoint. Here's an example `prometheus.yaml` file: - -```yaml -# Example prometheus.yaml -scrape_configs: - - job_name: "python-autometrics-example" - metrics_path: /metrics - static_configs: - - targets: ["localhost:8080"] - # For a real deployment, you would want the scrape interval to be - # longer but for testing, you want the data to show up quickly - scrape_interval: 500ms -``` +> Don't forget to configure Prometheus itself to scrape the metrics endpoint. Refer to the example `prometheus.yaml` file in the root of this project on how to set this up. ## `caller-example.py` Autometrics also tracks a label, `caller`, which is the name of the function that called the decorated function. The `caller-example.py` script shows how to use that label. It uses the same structure as the `example.py` script, but it prints a PromQL query that you can use to explore the caller data yourself. -Don't forget to configure Prometheus itself to scrape the metrics endpoint. Here's an example `prometheus.yaml` file: - -```yaml -# Example prometheus.yaml -scrape_configs: - - job_name: "python-autometrics-example" - metrics_path: /metrics - static_configs: - - targets: ["localhost:8080"] - # For a real deployment, you would want the scrape interval to be - # longer but for testing, you want the data to show up quickly - scrape_interval: 500ms -``` +> Don't forget to configure Prometheus itself to scrape the metrics endpoint. Refer to the example `prometheus.yaml` file in the root of this project on how to set this up. ## `fastapi-example.py` This is an example that shows you how to use autometrics to get metrics on http handlers with FastAPI. In this case, we're setting up the API ourselves, which means we need to expose a `/metrics` endpoint manually. -Don't forget to configure Prometheus itself to scrape the metrics endpoint. Here's an example `prometheus.yaml` file: - -```yaml -# Example prometheus.yaml -scrape_configs: - - job_name: "python-autometrics-example" - metrics_path: /metrics - static_configs: - - targets: ["localhost:8080"] - # For a real deployment, you would want the scrape interval to be - # longer but for testing, you want the data to show up quickly - scrape_interval: 500ms -``` +> Don't forget to configure Prometheus itself to scrape the metrics endpoint. Refer to the example `prometheus.yaml` file in the root of this project on how to set this up. diff --git a/prometheus.yaml b/prometheus.yaml index 2dd330e..ddd3b99 100644 --- a/prometheus.yaml +++ b/prometheus.yaml @@ -3,12 +3,15 @@ global: evaluation_interval: 15s scrape_configs: - - job_name: 'prometheus' + # Use prometheus to scrape prometheus :) + - job_name: "prometheus" scrape_interval: 5s static_configs: - - targets: ['localhost:9090'] + - targets: ["localhost:9090"] - - job_name: 'myservice' - scrape_interval: 5s + - job_name: "python-autometrics-example" + # For a real deployment, you would want the scrape interval to be + # longer but for testing, you want the data to show up quickly + scrape_interval: 500ms static_configs: - - targets: ['localhost:8080'] + - targets: ["localhost:8080"] From 1627b11b639b46380ad36c93394a8a00ac06d0fa Mon Sep 17 00:00:00 2001 From: Brett Beutell Date: Wed, 5 Apr 2023 19:07:01 +0200 Subject: [PATCH 7/7] Format the examples again --- examples/caller-example.py | 16 ++++++++++++---- examples/example.py | 6 ++++-- examples/fastapi-example.py | 1 + 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/caller-example.py b/examples/caller-example.py index 4e8720b..4239186 100644 --- a/examples/caller-example.py +++ b/examples/caller-example.py @@ -3,21 +3,25 @@ import time import random + # This is moana, who would rather explore the ocean than prometheus metrics @autometrics def moana(): return "surf's up!" + # This is neo, the one (that we'll end up calling) @autometrics def neo(): return "i know kung fu" + # This is simba. Rawr. @autometrics def simba(): return "rawr" + # Define a function that randomly calls `moana`, `neo`, or `simba` @autometrics def destiny(): @@ -28,14 +32,18 @@ def destiny(): return f"Destiny is calling neo. neo says: {neo()}" else: return f"Destiny is calling simba. simba says: {simba()}" - + # Start an HTTP server on port 8080 using the Prometheus client library, which exposes our metrics to prometheus start_http_server(8080) print(f"Try this PromQL query in your Prometheus dashboard:\n") -print(f"# Rate of calls to the `destiny` function per second, averaged over 5 minute windows\n") -print('sum by (function, module) (rate(function_calls_count_total{caller="destiny"}[5m]))') +print( + f"# Rate of calls to the `destiny` function per second, averaged over 5 minute windows\n" +) +print( + 'sum by (function, module) (rate(function_calls_count_total{caller="destiny"}[5m]))' +) # Enter an infinite loop (with a 1 second sleep period), calling the `destiny` and `agent_smith` methods. while True: @@ -43,4 +51,4 @@ def destiny(): time.sleep(0.3) -# NOTE - You will want to open prometheus +# NOTE - You will want to open prometheus diff --git a/examples/example.py b/examples/example.py index 86530aa..dca2fde 100644 --- a/examples/example.py +++ b/examples/example.py @@ -3,11 +3,12 @@ import time import random -# Defines a class called `Operations`` that has two methods: + +# Defines a class called `Operations`` that has two methods: # 1. `add` - Perform addition # 2. `div_handled` - Perform division and handle errors # -class Operations(): +class Operations: def __init__(self, **args): self.args = args @@ -27,6 +28,7 @@ def div_handled(self, num1, num2): result = e.__class__.__name__ return result + # Perform division without handling errors @autometrics def div_unhandled(num1, num2): diff --git a/examples/fastapi-example.py b/examples/fastapi-example.py index 9fe5964..bae7130 100644 --- a/examples/fastapi-example.py +++ b/examples/fastapi-example.py @@ -5,6 +5,7 @@ app = FastAPI() + # Set up a metrics endpoint for Prometheus to scrape # `generate_lates` returns the latest metrics data in the Prometheus text format @app.get("/metrics")