Plot your REST endpoints using grafana-infinity-datasource

When it comes to observability Grafana is the go-to tool for visualisation.
A Grafana dashboard consists of various forms of visualisations which are usually backed by a database.
This is not always the case. Sometimes instead of pushing the data from the database as is, you might want to refine the data. This cannot alway be achieved through the functionalities the db provides. For example you might want to fetch results from a proprietary api. This is where the grafana-infinity-datasource plugin kicks in. With the grafana-infinity-datasource you can create visualisations based on json, xml , csv etc. You can issue an http request towards a REST api and plot the received data.

 

Let’s assume we have an eShop application. We will create a simple python api using FastApi managing the items of the eShop and the purchase volume.
Through this api we will add items and purchase-volume entries.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
from datetime import datetime

app = FastAPI()

class Item(BaseModel):
    id: int
    name: str
    description: str = None
    price: float

class Purchase(BaseModel):
    price: float
    time: datetime

items = []
purchases = []

@app.post("/items/", response_model=Item)
def create_item(item: Item):
    items.append(item)
    return item

@app.get("/items/", response_model=List[Item])
def read_items():
    return items

@app.get("/items/{item_id}", response_model=Item)
def read_item(item_id: int):
    for item in items:
        if item.id == item_id:
            return item
    raise HTTPException(status_code=404, detail="Item not found")

@app.delete("/items/{item_id}", response_model=Item)
def delete_item(item_id: int):
    for idx, item in enumerate(items):
        if item.id == item_id:
            return items.pop(idx)
    raise HTTPException(status_code=404, detail="Item not found")

@app.post("/purchases/", response_model=Purchase)
def create_purchase(purchase: Purchase):
    purchases.append(purchase)
    return purchase

@app.get("/purchases/", response_model=List[Purchase])
def read_purchases():
    return purchases

also we need FastApi to be added to the requirements.txt

fastapi

We shall host the application through Docker thus we shall create a Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY main.py main.py

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

We should proceed to the Grafana visualisations.
Essentially we have two different sources of data.
The model Item will be visualized in a table and the model purchase will be visualized through a time series graph.

I shall use Docker Compose to provision Grafana as well as the python application

version: '3.8'

services:
  app:
    build: .
    ports:
      - 8000:8000
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    volumes:
      - ./grafana:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_USER=test
      - GF_SECURITY_ADMIN_PASSWORD=infinity
      - GF_INSTALL_PLUGINS=yesoreyeram-infinity-datasource

Essentially through the environment variable on Docker I enable the the infinity-datasource plugin.

We can get our instances up and running by issuing

docker compose up

Docker Compose V2 is out there with many good features, you can find more about it on the book I authored:
A Developer’s Essential Guide to Docker Compose
.

We can now populate the application with some data:

$ curl -X POST "http://127.0.0.1:8000/purchases/" -H "Content-Type: application/json" -d '{"time": "2024-07-15T12:40:56","price":2.5}'
$ curl -X POST "http://127.0.0.1:8000/purchases/" -H "Content-Type: application/json" -d '{"time": "2024-07-15T12:41:56","price":4.0}'
$ curl -X POST "http://127.0.0.1:8000/purchases/" -H "Content-Type: application/json" -d '{"time": "2024-07-15T12:42:56","price":1.5}'
$ curl -X POST "http://127.0.0.1:8000/purchases/" -H "Content-Type: application/json" -d '{"time": "2024-07-15T12:43:56","price":3.5}'

$ curl -X POST "http://127.0.0.1:8000/items/" -H "Content-Type: application/json" -d '{"id": 1, "name": "Item 1", "description": "This is item 1", "price": 10.5, "tax": 0.5}'

Onwards create a dashboard on Grafana.

One visualisation for items.

One visualisation for purchase volume.

As you can see in both cases I used the http://app:8000 endpoint, which is our application and the DNS that Compose application can resolve.

That’s it, we plotted our data from a REST api using Grafana.

PostgreSQL driver host Fail-over

Previously we setup BiDirectional replication for PostgreSQL.

You might have various legitimate reasons to do so and probably will get to that in another blog.
Overall picking this type of replication can be influenced a lot by the nature of your application, the need for active active dr scenarios and even cases of migration .

Since we have replication in place it would be great to examine the case of an outage and how we can utilise the native fail-over functionality of the PostgreSQL drivers.
We shall change the conflict resolution strategy to last_update_wins. This way between two simultaneous updates in each database the update with the max commit timestamp will be the one chosen one.

listen_addresses = '*'
port = 5432
max_connections = 20
shared_buffers = 128MB
temp_buffers = 8MB
work_mem = 4MB
wal_level = logical
max_wal_senders = 3
track_commit_timestamp = on
shared_preload_libraries = 'pglogical'
pglogical.conflict_resolution = 'last_update_wins'

We need to spin up the compose services with the new changes:

docker compose up

Docker Compose V2 is out there with many good features, you can find more about it on the book I authored:
A Developer’s Essential Guide to Docker Compose
.

Take note that based on the programming language and the driver, this functionality might not always be available. The concept is that when you configure the connection pool to establish connection to the database you can configure two hosts. The first host will be the primary one and the secondary host will be the one to fail-over once the primary host gets offline. The fail-over can be interchangeable, essentially the driver tries to find the first available host.

Python and the driver psycopg2 offer this functionality. We shall implement an app using the flask api. The app will give two endpoints, one for fetching an employee’s salary and one to increment the salary by 1:

from flask import Flask 
from psycopg2.pool import SimpleConnectionPool

app = Flask(__name__)

postgreSQL_pool = SimpleConnectionPool(1, 20, user="postgres",
                                       password="postgres",
                                       host="localhost,localhost",
                                       port="5432,5431",
                                       database="postgres",
                                       options="-c search_path=test_schema")


@app.route('/employee/<employee_id>/salary/increment', methods=['POST'])
def increment_salary(employee_id):
    conn = postgreSQL_pool.getconn()
    cur = conn.cursor()
    cur.execute("""
        UPDATE employee SET salary=salary + %s WHERE id = %s;
        """, (1, employee_id))
    conn.commit()
    cur.close()
    postgreSQL_pool.putconn(conn)
    return '', 204


@app.route('/employee/<employee_id>/salary')
def index(employee_id):
    conn = postgreSQL_pool.getconn()
    cur = conn.cursor()
    cur.execute("""
        SELECT salary FROM employee WHERE id=%s;
        """, employee_id)
    salary = cur.fetchone()[0]
    cur.close()
    postgreSQL_pool.putconn(conn)
    return str(salary), 200

Let’s example the SimpleConnectionPool, we can see two hosts separated with a comma (it’s localhost since it’s our local docker compose running) and on the port section the respective host ports are separated by comma.

We can run the app

flash run

And on another terminal issue the calls using curl

$ curl -X POST http://localhost:5000/employee/1/salary/increment
$ curl http://localhost:5000/employee/1/salary

Overall the salary will increase and we should see that on the get request.

Now let’s shut down one database

docker compose stop postgres-b

The first call after this operation will be a failed one, however the connection will be reinitialized and point to the secondary host.

% curl http://localhost:5000/employee/1/salary
<!doctype html>
<html lang=en>
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
%  curl http://localhost:5000/employee/1/salary
1254.23                              

The same functionality applies for other drivers. Take for example the Java driver configuration on a spring boot application.

spring.datasource.url=jdbc:postgresql://localhost:5432,localhost:5431/postgres?currentSchema=test_schema
spring.datasource.username=postgres
spring.datasource.password=postgres

On the jdbc url we add two hosts comma delimited localhost:5432,localhost:5431

Then we can implement an application with the same functionality.

package com.egkatzioura.psqlfailover.repository;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;


@Repository
public class EmployeeRepository {

    private final JdbcTemplate jdbcTemplate;

    public EmployeeRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }


    public void incrementSalary(Long employeeId, float increment) {
        jdbcTemplate.update("UPDATE employee SET salary=salary+? WHERE id=?",increment, employeeId);
    }

    public Float fetchSalary(Long employeeId) {
        return jdbcTemplate.queryForObject("SELECT salary FROM employee WHERE id=?",new Object[]{employeeId},Float.class);
    }
}

package com.egkatzioura.psqlfailover.controller;

import com.egkatzioura.psqlfailover.repository.EmployeeRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class EmployeeController {

    private final EmployeeRepository employeeRepository;

    public EmployeeController(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @PostMapping("/employee/{id}/salary/increment")
    public void incrementSalary(@PathVariable Long id) {
        employeeRepository.incrementSalary(id,1f);
    }

    @GetMapping("/employee/{id}/salary")
    public Float fetchSalary(@PathVariable Long id) {
        return employeeRepository.fetchSalary(id);
    }
}

Thanks to the replication the changes should have reached the other database. You can start and restart the compose services in a round robin fashion. The changes will be replicated and thus every time there is a fail-over the data will be there.

While we start and stop the databases docker compose stop postgres-b, we can issue requests using curl:

$ curl -X POST http://localhost:8080/employee/1/salary/increment
$ curl http://localhost:8080/employee/1/salary

Eventually the java driver handles the fail-over even more gracefully. Instead of failing on the first request during the fail-over instead it will fisr to connect to the other host and give back the results.

That’s it. You setup BiDirectional replication on PostgreSQL and you managed to take advantage of the driver capabilities to fail-over to different hosts. Hope you had some fun!

Embed Jython to you java codebase.

Jython is a great tool for some quick java scripts using a pretty solid syntax. Actually it works wonderfully when it comes to implement some maintenance or monitoring scripts with jmx for you java apps.

In case you work with other teams with a python background, it makes absolute sense to integrate python to your java applications.

First let’s import the jython interpeter using the standalone version.

group 'com.gkatzioura'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.5

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.11'
    compile group: 'org.python', name: 'jython-standalone', version: '2.7.0'
}

So the easiest thing to do is just to execute a python file in our class path. The file would be hello_world.py

print "Hello World"

And then pass the file as an inputstream to the interpeter

package com.gkatzioura;

import org.python.core.PyClass;
import org.python.core.PyInteger;
import org.python.core.PyObject;
import org.python.core.PyObjectDerived;
import org.python.util.PythonInterpreter;

import java.io.InputStream;

/**
 * Created by gkatzioura on 19/10/2016.
 */
public class JythonCaller {

    private PythonInterpreter pythonInterpreter;

    public JythonCaller() {
        pythonInterpreter = new PythonInterpreter();
    }

    public void invokeScript(InputStream inputStream) {

        pythonInterpreter.execfile(inputStream);
    }

}
    @Test
    public void testInvokeScript() {

        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("hello_world.py");
        jythonCaller.invokeScript(inputStream);
    }

Next step is to create a python class file and and another python file that will import the class file and instantiate a class.

The class file would be divider.py.

class Divider:

    def divide(self,numerator,denominator):

        return numerator/denominator;

And the file importing the Divider class would be classcaller.py

from divider import Divider

divider = Divider()

print divider.divide(10,5);

So let us test it

    @Test
    public void testInvokeClassCaller() {

        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("classcaller.py");
        jythonCaller.invokeScript(inputStream);
    }

What we can understand from this example is that the interpreter imports successfully the files from the classpath.

Running files using the interpreter is ok, however we need to fully utilize classes and functions implemented in python.
Therefore next step is to create a python class and use its functions using java.

package com.gkatzioura;

import org.python.core.PyClass;
import org.python.core.PyInteger;
import org.python.core.PyObject;
import org.python.core.PyObjectDerived;
import org.python.util.PythonInterpreter;

import java.io.InputStream;

/**
 * Created by gkatzioura on 19/10/2016.
 */
public class JythonCaller {

    private PythonInterpreter pythonInterpreter;

    public JythonCaller() {
        pythonInterpreter = new PythonInterpreter();
    }

    public void invokeClass() {

        pythonInterpreter.exec("from divider import Divider");
        PyClass dividerDef = (PyClass) pythonInterpreter.get("Divider");
        PyObject divider = dividerDef.__call__();
        PyObject pyObject = divider.invoke("divide",new PyInteger(20),new PyInteger(4));

        System.out.println(pyObject.toString());
    }

}

You can find the sourcecode on github.

Implement a SciPy Stack Docker Image

SciPy is a powerful python library, but it has many dependencies including Fortran.
So Running your Scipy code in a docker container makes absolute sense.

We will use a private registry

docker run -d -p 5000:5000 --name registry registry:2

I will use a Centos image.
Centos is a very popular linux distribution based on RedHat which is a commercial Linux distribution. Oracle’s Linux and Amazon Linux is based on Red Hat Linux.

docker pull centos
docker tag centos localhost:5000/centos
docker push localhost:5000/centos

Then we start a container

docker run -i -t --name centoscontainer localhost:5000/centos /bin/bash

We install all binary dependencies

yum install -y epel-release
yum -y update
yum -y groupinstall "Development Tools"
yum -y install python-devel
yum -y install blas --enablerepo=epel
yum -y install lapack --enablerepo=epel
yum -y install Cython --enablerepo=epel
yum -y install python-pip

Then we install the scipy stack

pip install boto3
pip install numpy
pip install pandas
pip install scipy

And we are ready. Now we should proceed on committing the image.

docker commit -m 'Added scipy stack' -a "Emmanouil Gkatziouras" 4954f603d93b localhost:5000/scipy
docker push localhost:5000/scipy

Now we are ok to run our SciPy enabled container.

docker run -t -i localhost:5000/scipy /bin/bash

Last but not least we clear our registry.

docker stop registry && docker rm -v registry

Connecting to JMX through Jython

Jython is great when you want a dynamically typed language based on the JVM.

Also comes really in handy when you want to write small monitoring scripts based on JMX.This is an examble on how to call a function from a MBean through Jython using JMX.

jmxaction.py

from javax.management.remote import JMXConnector
from javax.management.remote import JMXConnectorFactory
from javax.management.remote import JMXServiceURL
from javax.management import MBeanServerConnection
from javax.management import MBeanInfo
from javax.management import ObjectName
from java.lang import String

from jarray import array
import sys   

if __name__=='__main__':
        
        if len(sys.argv)> 5:
                serverUrl = sys.argv[1]
                username = sys.argv[2]
                password = sys.argv[3] 
                beanName = sys.argv[4]
                action = sys.argv[5]
        else:
                sys.exit(-1)
        credentials = array([username,password],String)
        environment = {JMXConnector.CREDENTIALS:credentials}

        jmxServiceUrl = JMXServiceURL('service:jmx:rmi:///jndi/rmi://'+serverUrl+':9999/jmxrmi');
        jmxConnector = JMXConnectorFactory.connect(jmxServiceUrl,environment);
        mBeanServerConnection = jmxConnector.getMBeanServerConnection()
        objectName = ObjectName(beanName);
        mBeanServerConnection.invoke(objectName,action,None,None)
        jmxConnector.close()

By calling the script

jython jmxaction.py {ip} {jmx user} {jmx password} {mbean name} {action}

You can invoke the action of the mbean specified.

Pyftpdlib : An ftp server python library

In one of my projects there was the need to provide an ftp file transfer. So instead of setting up a ftp server I decided to make one of my own using the pyftpdlib library and add custom actions once a file was received or transferred. The other advantage was the fact that the ftp server would have been part of the existing project’s python software.

Here’s an example in case you want a custom handler and you don’t have the time to read the ftpserver.py source.

from pyftpdlib import ftpserver
from threading import Thread

class PythoFtpServer(Thread):
    
    def __init__(self): 
        Thread.__init__(self)
        self.daemon = True
        authorizer = ftpserver.DummyAuthorizer()
        authorizer.add_user("usr","pwd", "adir",perm='elradfmw')
        handler = CustomFtpHandler
        handler.authorizer = authorizer
        address = ('127.0.0.1',1024)
        self.server = ftpserver.FTPServer(address, handler)

    def run(self):
        Thread.run(self)
        self.server.serve_forever()

class CustomFtpHandler(ftpserver.FTPHandler):
    
    def on_file_sent(self, file):
        """Called every time a file has been succesfully sent.
        "file" is the absolute name of the file just being sent.
        """

    def on_file_received(self, file):
        """Called every time a file has been succesfully received.
        "file" is the absolute name of the file just being received.
        """

    def on_incomplete_file_sent(self, file):
        """Called every time a file has not been entirely sent.
        (e.g. ABOR during transfer or client disconnected).
        "file" is the absolute name of that file.
        """

    def on_incomplete_file_received(self, file):
        """Called every time a file has not been entirely received
        (e.g. ABOR during transfer or client disconnected).
        "file" is the absolute name of that file.
        """

    def on_login(self, username):
        """Called on user login."""

    def on_login_failed(self, username, password):
        """Called on failed user login.
        At this point client might have already been disconnected if it
        failed too many times.
        """

    def on_logout(self, username):
        """Called when user logs out due to QUIT or USER issued twice."""

Thanks to Giampaolo Rodola for this library 🙂

Pyftpdib @ Goole code