Flask application monitoring with Prometheus

Intro

Prometheus is an open source project that is part of the Cloud Native Computing Foundation(CNCF). It serves as a monitoring system and time series database. The project was second to be part of CNCF after Kubernetes. As such the two project play really nicely with one another.

In this post I'll take us through using Kubernetes(K8s for short) as our deployment platform for Prometheus, then setting up a simple python app for monitoring. I recommend checking out my previous post on standing up K8s on AWS first, here. Otherwise if you already have a K8s cluster, lets get started.

Prerequisites

  • A running Kubernetes cluster
  • Configured kubectl client

Architecture

Not going to do a complete architecture run through here, but I want to mention some of the basics.

Prometheus takes a particular stance on how it goes about monitoring. Most monitoring systems out there rely on the use of an agent, that sits on the client machine. This agent will gather metric from the host and shoot them over to the monitoring system. This can be seen as a push method.

Prometheus takes more of a pull method to this. The idea behind this is that you tell Prometheus what it needs to monitor, and it will go out and start scraping that endpoint for metric.

This approach provides a lot of flexibility as application KPIs can be added/modified/removed very quickly and provides a modern devops approach to application monitoring. It also means application monitoring is driven by the individuals/teams who understand the application(to be monitored) best.

Jump in

If you want to jump straight in, deploy the following in your k8s cluster:

  • python-app - simple python app that incorporates Prometheus monitoring. Metrics provides by app: request_processing_seconds, index_request_processing_seconds, requests_for_host.

  • Prometheus - The official Prometheus image.

  • Assets - All assets described in the post can be retrieved from this repo

Deploy Prometheus

Deploying Prometheus into K8s is super easy. Lets take a look at the deployment file deployment.yml.

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: prometheus-deployment-1.7.1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: prometheus-server
    spec:
      containers:
        - name: prometheus
          image: prom/prometheus:v1.7.1
          args:
            - "-config.file=/etc/prometheus/conf/prometheus.yml"
            # Metrics are stored in an emptyDir volume which
            # exists as long as the Pod is running on that Node.
            # The data in an emptyDir volume is safe across 
            # container crashes.
            - "-storage.local.path=/prometheus/"
          ports:
            - containerPort: 9090
          volumeMounts:
            - name: prometheus-config-volume
              mountPath: /etc/prometheus/conf/
            - name: prometheus-storage-volume
              mountPath: /prometheus/
      volumes:
        # The config map we will create then bind to our volume mount.
        - name: prometheus-config-volume
          configMap:
            name: prometheus-server-conf
        # Create the actual volume for the metric data
        - name: prometheus-storage-volume
          emptyDir: {} # containers in the Pod can all read and write the same files here.

Before we deploy this let first create the configmap for the prometheus config. A configmap is a way we can decouple and store configuration files for use in different deployments. Lets looks at the prometheus.yml config.

global:
  scrape_interval: 5s
  evaluation_interval: 5s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'python-app'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app]
        regex: python-app
        action: keep

Here we have setup two scrape jobs. One is simply telling Prometheus to scrape itself. The other is scraping our example python-app. Take notice of the kubernetes_sd_configs directive, very handy for using the kubernetes api to find scrape targets. Here we are using the pods role to discover all pods with an app label of 'python-app'. Also it is worth mentioning that by default Prometheus will scrape the /metrics endpoint unless stated otherwise.

We can create the configmap like so:

kubectl create configmap prometheus-server-conf --from-file=prometheus.yml

Now that we have created that we can create our prometheus deployment.

kubectl create -f deployment.yml

Once you have deployed Prometheus you will be able to see your deployment via

kubectl get deploy
NAME                          DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
prometheus-deployment-1.7.1   1         1         1            1           40s

Now that Prometheus has been deployed, we need a method of accessing it. For this we will need to create a service(svc). A service manifest file looks like so:

apiVersion: v1
kind: Service
metadata:
  name: prometheus-service
spec:
  selector: # exposes any pods with the following labels as a service
    app: prometheus-server
  type: NodePort
  ports:
    - port: 80 # this Service's port (cluster-internal IP clusterIP)
      targetPort: 9090 # pods expose this port
      # Kubernetes master will allocate a port from a flag-configured range (default: 30000-32767),
      # or we can set a specific port number (in our case).
      # Each node will proxy 32514 port (the same port number on every node) into this service.
      # Note that this Service will be visible as both NodeIP:nodePort and clusterIp:port
      nodePort: 32514

We can deploy a service in a similar fashion as the deployment:

kubectl create -f svc.yml

Now that we have a service for prometheus we can access it simply by hitting the node on port 32514 as we specified. Take note, if you are using aws then use the public ip of the instance (make sure your security group allows you access over this port).

Deploy Python-app

Now that Prometheus has been deployed we can now deploy our simple python app and start scraping some metric in. Our deployment file my-app.yml:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: python-app
spec:
  replicas: 1
  minReadySeconds: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    metadata:
      labels:
        app: python-app
    spec:
      containers:
      - name: python-app
        image: vect0r/python-app:latest
        ports:
        - containerPort: 5000
          name: web-app

Lets deploy this:

kubectl create -f my-app.yml

As before we'll create a service manifest, my-app-svc.yml:

apiVersion: v1
kind: Service
metadata:
  name: my-app-svc
spec:
  selector: # exposes any pods with the following labels as a service
    app: python-app
  type: NodePort
  ports:
    - port: 5000 # this Service's port (cluster-internal IP clusterIP)
      targetPort: 5000 # pods expose this port

Notice here we haven't specified a nodePort. Kubernetes will pick one out for us. Create the service:

kubectl create -f my-app-svc.yml
kubectl get svc
NAME                  CLUSTER-IP      EXTERNAL-IP   PORT(S)                         AGE
prometheus-service    100.69.192.22   <nodes>       80:32514/TCP                    13d
my-app-svc            100.69.92.128   <nodes>      5000:32455/TCP   12d

Listing the services displays the nodePort selected, we can see the python app can be accessed on 32455. Remember back when we created our configmap for prometheus. We specified in our config to find pods with the 'python-app' app tag and scrape them. Hence now we have done this we should see the following targets in Prometheus.

App monitoring

We have two views we are monitoring in our web app. These are the '/' and '/host' endpoints. Lets take a look at our index view:

# Create a metric to track time spent and requests made.
INDEX_TIME = Summary('index_request_processing_seconds', 'DESC: INDEX time spent processing request')

# Create a metric to count the number of runs on process_request()
c = Counter('requests_for_host', 'Number of runs of the process_request method', ['method', 'endpoint'])

@app.route('/')
@INDEX_TIME.time()
def hello_world():
    path = str(request.path)
    verb = request.method
    label_dict = {"method": verb,
                 "endpoint": path}
    c.labels(**label_dict).inc()

    return 'Flask Dockerized'

This block of the code is doing serveral things, firstly we are creating a Summary metric type. This is one of the metric type the prometheus_client supports. The first parameter is the name of the metric, the second is a description text for the metric. The second metric we create is a Counter, similar to the Summary the first two parameters are name and description. However a third parameter is provided, this allows us to provide tags to differentiate targets. Think of it as allowing you to create another line on the graph.

Then we create our view using the flask decorator, notice we also use INDEX_TIME as a decorator and using the time() method to give us the time taken to execute this method. The view itself is just returning 'Flask Dockerized' to the webpage, however note that we are using flask to get the path requested and the HTTP verb used. We save this to a dictionary and pass it through to our counter.

Expose metrics

How do we expose this to Prometheus? Well as mentioned before the Prometheus by default looks at the /metrics endpoint for all it's scraping. So lets build that endpoint into our app.

@app.route('/metrics')
def metrics():
    return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST)

What we have done here is simply used Flask's routing mechanism to create the metrics endpoint. This view is returning a Flask Response object. the generate_latest() method is provided by the Prometheus client and this will provide a string with all the metrics defined in the app and their values, in the Prometheus standard. We wrap this in the Response object that is passed back to the client. Output looks like below:
alt
Every time Prometheus scrapes this app, this block of code will run providing Prometheus with the latest metrics values, which it stores in its TSDB.

Graph in action

Lets take a look at what the code does for us. Navigate to your Prometheus UI and search on our metric 'index_request_processing_seconds', you will notice there is a count and sum. Every time you hit the index endpoint you will see both these metrics change. The count will show you how many times the view has been called, the sum will show you the total execution time taken by this method. You can see this in action below:
alt

The Counter metric we created can be found by searching requests_for_host.
alt
Here notice we have two lines one for our '/index' view and another for our '/hosts' view.

Wrap up

So we've gone go over deploying Prometheus on Kubernetes and using the Prometheus_client to monitor our app. This has only scratched the surface of what you can do. I hope this has help you kick start your use of Prometheus.