{"id":17794236,"url":"https://github.com/pyk/sanic-instrumentation-guide","last_synced_at":"2026-01-20T00:35:39.961Z","repository":{"id":39762380,"uuid":"127441083","full_name":"pyk/sanic-instrumentation-guide","owner":"pyk","description":"A guide on how to collect metrics from your Sanic application","archived":false,"fork":false,"pushed_at":"2022-12-08T00:56:02.000Z","size":11,"stargazers_count":3,"open_issues_count":5,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-13T15:55:34.927Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pyk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-03-30T15:05:51.000Z","updated_at":"2023-11-06T17:41:23.000Z","dependencies_parsed_at":"2022-08-28T03:02:51.626Z","dependency_job_id":null,"html_url":"https://github.com/pyk/sanic-instrumentation-guide","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyk%2Fsanic-instrumentation-guide","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyk%2Fsanic-instrumentation-guide/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyk%2Fsanic-instrumentation-guide/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyk%2Fsanic-instrumentation-guide/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pyk","download_url":"https://codeload.github.com/pyk/sanic-instrumentation-guide/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247658498,"owners_count":20974524,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-27T11:15:36.700Z","updated_at":"2026-01-20T00:35:39.935Z","avatar_url":"https://github.com/pyk.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# A Guide to Instrument Sanic Application\n\n**NOTE: This guide is in progress**\n\nIn this repository contains a guide on how to collect metrics from\nyour Sanic Application.\n\nTODO: add example dashboard here\n\nThese are metrics that we will collect from our Sanic application:\n\n* Request rate (per second)\n* Request duration (in millisecond)\n* Error rate (4xx or 5xx responses)\n\n\n## Instrumentation Stack\nWe will use these tools:\n\n* [Prometheus](https://prometheus.io): For storing the metrics data\n* [Grafana](https://grafana.com/): For exploring the metrics data\n\n### Prometheus\nPrometheus is an open-source systems monitoring and alerting toolkit originally\nbuilt at SoundCloud. You can install and run the prometheus by following this\n[guide](https://prometheus.io/docs/introduction/first_steps/).\n\nFor this guide, I use a docker container to run the prometheus instance:\n\n```bash\ndocker run --rm -p 9090:9090 \\\n    -v $PWD/prometheus/:/etc/prometheus/ \\\n    prom/prometheus:v2.1.0 \\\n    --config.file=/etc/prometheus/config.yaml \\\n    --storage.tsdb.path=/etc/prometheus/data\n```\n\nThe prometheus instance is accessible at [http://localhost:9090](http://localhost:9090).\n\n### Grafana\nGrafana is the open platform for beautiful analytics and monitoring. You can install\nand run the Grafana by following this [guide](https://grafana.com/get).\n\nFor this guide, I use a docker container to run the grafana instance:\n\n```bash\ndocker run --rm -p 3000:3000 \\\n      -v $PWD/grafana:/var/lib/grafana \\\n      grafana/grafana:5.0.4\n```\n\nThe grafana instance is accessible at [http://localhost:3000](http://localhost:3000).\n\n\n## Tutorial\nSuppose that we have the following Sanic app that we want to monitor:\n\n```python\n# app.py\nimport asyncio\nimport random\n\nfrom sanic import Sanic\nfrom sanic import response\n\napp = Sanic()\n\n\n@app.get(\"/\")\nasync def index(request):\n    # Simulate latency: 0ms to 1s\n    latency = random.random()  # in seconds\n    await asyncio.sleep(latency)\n    return response.json({\"message\": \"Hello there!\"})\n\n\n@app.get(\"/products\")\nasync def products(request):\n    products = [\n        {\"title\": \"product_a\", \"price\": 10.0},\n        {\"title\": \"product_b\", \"price\": 5.0},\n    ]\n    # Simulate latency: 0ms to 1s\n    latency = random.random()  # in seconds\n    await asyncio.sleep(latency)\n    return response.json(products)\n\n\n@app.post(\"/order\")\nasync def order(request):\n    # Simulate latency: 0ms to 1s\n    latency = (1 - random.random())  # in seconds\n    await asyncio.sleep(latency)\n    return response.json({\"message\": \"OK\"})\n\n\nif __name__ == \"__main__\":\n    app.run(host=\"0.0.0.0\", port=8080)\n```\n\nThere are 3 dummy endpoints:\n\n1. `GET /`. It returns a simple JSON that says hello.\n2. `GET /prodcuts`. It returns a list of dummy products.\n3. `POST /order`. It post an dummy order and returns OK.\n\nOur goal is to to be able to get these following metrics from\nall endpoints:\n\n1. Request rate (per second)\n2. Request duration (in millisecond)\n3. Error rate (4xx or 5xx response)\n\n\n### How to get, store and display the metrics data\nPrometheus collect the data from our Sanic application by scraping `/metrics` endpoint.\nSo our step-by-step is:\n\n1. Collect metrics from our application.\n2. Expose the collected metrics via `/metrics` endpoint.\n3. Add new job configuration for Prometheus.\n4. Query the data on the grafana dashboard.\n\nWe will get into details for each step on the section below.\n\nFor the metrics collection, we will use the official\n[Prometheus Python Client](https://github.com/prometheus/client_python)\nto collect metrics from our Sanic application. Run the following command to\nget the package:\n\n    pip install -U prometheus_client\n\nSo in the next section, we will start collecting request rate from our Sanic app.\n\n\n### Request Rate\nIn this section we are going to collect the request rate data and display the value\nto the Grafana dashboard.\n\nFirst of all, we need to understand what kind of metrics that we can store in\nthe Prometheus. Currently, there are 4 types of metric in the Prometheus\nthat we can use to represent our data:\n\n1. [Counter](https://prometheus.io/docs/concepts/metric_types/#counter).\n   A counter is a cumulative metric that represents a single numerical\n   value that only ever goes up. A counter is typically used to count\n   requests served, tasks completed, errors occurred, etc.\n2. [Gauge](https://prometheus.io/docs/concepts/metric_types/#gauge).\n   A gauge is a metric that represents a single numerical value that\n   can arbitrarily go up and down.\n3. [Histogram](https://prometheus.io/docs/concepts/metric_types/#histogram).\n   A histogram samples observations (usually things like request durations\n   or response sizes) and counts them in configurable buckets. It also\n   provides a sum of all observed values.\n4. [Summary](https://prometheus.io/docs/concepts/metric_types/#summary).\n   Similar to a histogram, a summary samples observations (usually things\n   like request durations and response sizes). While it also provides a\n   total count of observations and a sum of all observed values, it\n   calculates configurable quantiles over a sliding time window.\n\nFor request rate metric, we will use `Counter` metric type.\n\nThe first step that we do is import the prometheus client and initialize\nour Counter metric:\n\n```python\nimport prometheus_client as prometheus\n\n# Initialize the metrics\ncounter = prometheus.Counter(\"sanic_requests_total\",\n                             \"Track the total number of requests\",\n                             [\"method\", \"endpoint\"])\n```\n\n`\"sanic_requests_total\"` is the name of our metric, we must follow the\n[Prometheus guideline](https://prometheus.io/docs/practices/naming/) for this.\nThere is a description and the label `[\"method\", \"endpoint\"]` to helps us to\ndistinguish each request for which endpoint.\n\nSince we want to track all requests on all endpoints, we can use\n[middleware](http://sanic.readthedocs.io/en/latest/sanic/middleware.html)\nto achieve this.\n\n```python\n# Track the total number of requests\n@app.middleware('request')\nasync def track_requests(request):\n    # Increase the value for each request\n    # pylint: disable=E1101\n    counter.labels(method=request.method,\n                   endpoint=request.path).inc()\n```\n\nThis middleware will increase our Counter value based on the `request.method`\n(`GET`, `POST`, etc) and the `request.path` (`/`, `/products` etc).\n\nSo we already track all of the requests, the next step is to expose the\n`/metrics` endpoint to be scraped by Prometheus.\n\n```python\n# Expose the metrics for prometheus\n@app.get(\"/metrics\")\nasync def metrics(request):\n    output = prometheus.exposition.generate_latest().decode(\"utf-8\")\n    content_type = prometheus.exposition.CONTENT_TYPE_LATEST\n    return response.text(body=output,\n                         content_type=content_type)\n```\n\nSo here is the full implementation:\n\n```python\n\n# app.py\nimport asyncio\nimport random\n\nfrom sanic import Sanic\nfrom sanic import response\nimport prometheus_client as prometheus\n\n\napp = Sanic()\n\n# Initialize the metrics\ncounter = prometheus.Counter(\"sanic_requests_total\",\n                             \"Track the total number of requests\",\n                             [\"method\", \"endpoint\"])\n\n\n# Track the total number of requests\n@app.middleware(\"request\")\nasync def track_requests(request):\n    # Increase the value for each requests\n    # pylint: disable=E1101\n    counter.labels(method=request.method,\n                   endpoint=request.path).inc()\n\n\n# Expose the metrics for prometheus\n@app.get(\"/metrics\")\nasync def metrics(request):\n    output = prometheus.exposition.generate_latest().decode(\"utf-8\")\n    content_type = prometheus.exposition.CONTENT_TYPE_LATEST\n    return response.text(body=output,\n                         content_type=content_type)\n\n\n@app.get(\"/\")\nasync def index(request):\n    # Simulate latency: 0ms to 1s\n    latency = random.random()  # in seconds\n    await asyncio.sleep(latency)\n    return response.json({\"message\": \"Hello there!\"})\n\n\n@app.get(\"/products\")\nasync def products(request):\n    products = [\n        {\"title\": \"product_a\", \"price\": 10.0},\n        {\"title\": \"product_b\", \"price\": 5.0},\n    ]\n    # Simulate latency: 0ms to 1s\n    latency = random.random()  # in seconds\n    await asyncio.sleep(latency)\n    return response.json(products)\n\n\n@app.post(\"/order\")\nasync def order(request):\n    # Simulate latency: 0ms to 1s\n    latency = (1 - random.random())  # in seconds\n    await asyncio.sleep(latency)\n    return response.json({\"message\": \"OK\"})\n\n\nif __name__ == \"__main__\":\n    app.run(host=\"0.0.0.0\", port=8080)\n```\n\nNow you can run your Sanic app and it will collect the metrics for each requests.\nHowever it is not stored in the Prometheus yet. We need to tell the Prometheus\ninstance where to to scrap the metrics data. Add the following job definition\nto the prometheus [config file](https://prometheus.io/docs/introduction/first_steps/#configuring-prometheus)\nand restart your Prometheus instance:\n\n```yaml\n- job_name: sampleapp\n  scrape_interval: 15s\n  scrape_timeout: 10s\n  metrics_path: /metrics\n  scheme: http\n  static_configs:\n  - targets:\n    - host.docker.internal:8080\n```\n\nI use `host.docker.internal` as the host of my Sanic app because\nI run it on my local machine. My localhost is accessible inside docker\ncontainer using `host.docker.internal` as the host.\n\nAccess your prometheus instance [http://localhost:9090/graph](http://localhost:9090/graph)\nand make sure the metrics data is available:\n\nTODO: continue here\n\n#### Questions\n\n- Sometimes we restart or re-deploy our Sanic application, we may ask what happens when the process restarts and the counter is reset to 0?\n  This is a common case, luckily the [rate()](https://prometheus.io/docs/prometheus/latest/querying/functions/#rate)\n  function in Prometheus will automatically handle this for us. So it is okay if\n  the Sanic application process is restarted and the value is resetted to zero,\n  nothing bad will happen.\n- The value of the counter is always increase, we may ask what happens when the value of the counter is overflowing?\n  The prometheus client is using a\n  [float value that protected by a mutex](https://github.com/prometheus/client_python/blob/master/prometheus_client/core.py#L315).\n  When the value reach larger than `sys.float_info.max`, it will returns `+Inf` as the value.\n  This will cause your graph displaying a zero flatline on the related timestamp. The current\n  solution is to restart your sanic application. It depends on your traffic volumes and\n  your deployment frequencies, this overflowing case may never happen.\n\n\nResources:\n- [How does a Prometheus Counter work?](https://www.robustperception.io/how-does-a-prometheus-counter-work/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpyk%2Fsanic-instrumentation-guide","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpyk%2Fsanic-instrumentation-guide","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpyk%2Fsanic-instrumentation-guide/lists"}