Hey friends, and welcome to the next Adventure of Blink! If you've been following along, we've done a ton of cool stuff this season:
- We learned about Docker
- We configured a MongoDB (in a Docker container with persistent storage)
- We made a Flask API for the database (also in a Docker container)
- We've explored Test-Driven Development practices with PyTest
- We made our tests run every time we commit to our repository using GitHub Actions
- We created a graphical interface for our program using Tkinter
- We scanned our project for security vulnerabilities using Snyk
...suffice to say, we've been really busy. But we're not through yet! Today we're covering another oft-overlooked topic:
Observability!
Observability is a core component of the DevOps mindset... because it's a place where Dev and Ops can easily interact. Ops is usually on the receiving end of support tickets and user complaints... but it's hard to diagnose something like "the application is slow!" without firm evidence of what happened. But if your developers aren't considering metrics and observability behavior when they're coding, you're not going to have those metrics for Ops to confirm a user's complaint.
TL/DR: Youtube
When to apply metrics
The answer to this is... as early as possible! Build metrics into your code while you're writing it, and become accustomed to using them throughout the development process.
Why are they important?
Using metrics in the development process ensures that you understand from the beginning how the application behaves. You'll want to consider things like load testing as you complete your work, ensuring that you see how your code behaves when there are lots of users running it at once.
How metrics are created
We're going to introduce two products to our application environment: Prometheus and Grafana.
Prometheus is a collection mechanism for metrics that we establish in our code. It runs in a Docker container as part of our environment and listens for metrics to be sent by our code... yes, that means we have some code changes to make, but it should be pretty easy work.
What metrics are important to us?
In our Hangman game, there's not a lot of processing going on. As a result, adding metrics for the performance of the application itself? Probably not all that useful.
A place where metrics would be useful would be around the API calls. If anything's going to malfunction, it's going to be the data extraction code... after all, that's the place where multiple containers get involved and where data has to flow from one system to another seamlessly. So we'll add our metrics instrumentation to the API code.
Setting up the tools
First, let's add the Prometheus and Grafana containers to our docker-compose.yml
file:
prometheus: image: prom/prometheus:latest volumes: # This prometheus.yml file we will create shortly 😉 - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: - "9090:9090" mongo-exporter: image: bitnami/mongodb-exporter:latest environment: # Note the name of our mongo container here MONGODB_URI: "mongodb://mongo:27017" depends_on: - mongo grafana: image: grafana/grafana:latest ports: - "3000:3000" depends_on: - prometheus
Next, let's build the prometheus.yml
file that establishes the configuration for Prometheus:
global: scrape_interval: 15s scrape_configs: - job_name: 'flask-api' # We'll have to create a /metrics endpoint in the API... metrics_path: '/metrics' # Endpoint from Flask app static_configs: # This target is our api container and port - targets: ['hangman-api:5001'] - job_name: 'mongo' metrics_path: '/metrics' # Endpoint for the MongoDB Exporter static_configs: - targets: ['mongo-exporter:9216'] # Default port for MongoDB Exporter
That leads us to make our code changes in the API:
from flask import Flask, jsonify, request # Adding in prometheus_client to help us build the metrics additions from prometheus_client import generate_latest, Counter from prometheus_client import multiprocess, CollectorRegistry, Gauge, Histogram from prometheus_client import multiprocess from pymongo import MongoClient from pymongo.errors import PyMongoError from bson.objectid import ObjectId from datetime import datetime import os app = Flask(__name__) # MongoDB connection mongo_uri = os.getenv("MONGO_URI_API") db_name = os.getenv("DB_NAME") collection_name = os.getenv("COLLECTION_NAME") # When testing locally, we bypass the .env and load the variables manually # mongo_uri = "mongodb://blink:theadventuresofblink@localhost:27017/hangman?authSource=admin" # db_name = "hangman" # collection_name = "phrases" client = MongoClient(mongo_uri) db = client[db_name] collection = db[collection_name] # Here's where we set up the metrics objects we're going to need: REQUEST_COUNT = Counter('flask_app_requests_total', 'Total number of requests to the app') REQUEST_LATENCY = Histogram('flask_app_request_latency_seconds', 'Latency of requests to the app') # This route is used by prometheus to extract the metrics. # generate_latest() is a library method that knows how to get # all prometheus_client objects and send them for the application # to pick up. @app.route('/metrics') def metrics(): return generate_latest() @app.route('/getall', methods=['GET']) def get_all_items(): # This is an example of how to instrument a method. # Notice we increment the request count, and then # the request latency is measured by putting the entire # method's code inside a With statement that captures # its timing REQUEST_COUNT.inc() with REQUEST_LATENCY.time(): try: # Find all records in the collection words = list(collection.find({}, {"_id": 0})) # Exclude _id field from the response return jsonify(words), 200 except Exception as e: return jsonify({"error": str(e)}), 500
For brevity's sake I didn't include the rest of the API code... but each route needs to be instrumented individually. You can add more metrics if you'd like to observe different behaviors separately.
Another note: make sure you add prometheus into the API's requirements.txt!
blinker==1.8.2 click==8.1.7 dnspython==2.7.0 Flask==3.0.3 itsdangerous==2.2.0 Jinja2==3.1.4 MarkupSafe==3.0.2 prometheus_client==0.21.0 pymongo==4.10.1 Werkzeug==3.1.1
Validating that it all works
Now that we've finished setup, we can start up our application:
# Windows/Unix docker-compose up --build # Mac docker compose up --build
We can see our new containers on ports 9090 (Prometheus) and 3000 (Grafana). Let's start in Prometheus, and set up some metrics queries:
...
Then once we've got them created, we can head over to Grafana to visualize them:
...
Wrapping up
These examples are small and somewhat contrived, in keeping with our theme of exploring these concepts in a small app so we can see how they work without the distraction of scale and complexity. But hopefully you can see from even these examples how much power you have as a developer to see what's happening within your code! This may seem like a lot of work for something that doesn't actually make our game any better or more interesting to play, but the value of metrics is in being able to diagnose more easily when something isn't right.
I hope you've learned a lot this week! We are nearly to the end of Season 2, and I'll tell ya what... it has been such a ride. Our season finale is going to be the long-awaited AI integration - we're going to let a Large Language Model build hangman games for us to play! So tune in next week for another Adventure of Blink!
Top comments (0)