In this example we are going to record how long it takes to return a response from controller methods and visualise the data. For visualising, we will use Grafana. Our application is stored on one server while InfluxDB and Grafana are installed on another server.


System


Application server: 192.168.99.10

InfluxDB server: 192.168.99.20
InfluxDB port: 8086
InfluxDB version: 1.5.1
Grafana version: v5.0.4

Our database is called response_stats and the table (measurement) is called points in InfluxDB.


Connection test


Check to see if application server can connect to remote server.


$ nc -zv 192.168.99.20 8086
Connection to 192.168.99.20 8086 port [tcp/*] succeeded!

Prerequisites


Make sure that the time on both servers are same otherwise you either won't be able to see graphs or see inadequate results in graphs. You can use NTP to set it and use $ timedatectl to verify it. Prevent unauthenticated access to database by enabling auth-enabled in /etc/influxdb/influxdb.conf configuration file of InfluxDB. Before you do that, you must create a user in database.


Installation


Install InfluxDB library with $ composer require influxdb/influxdb-php command. For more information, you can InfluxDB client library for PHP page.


Application


Just make our tests easier, we will use endpoints below.



parameters.yml


parameters:
...
influx_host: 192.168.99.20
influx_port: 8086
influx_user: inanzzz
influx_pass: 123123

config.yml


imports:
...
- { resource: '@AppBundle/Resources/config/' }

services.yml


services:
_defaults:
autowire: true
public: false

AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository,Tests}'

AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
public: true
tags: ['controller.service_arguments']

AbstractController


Just for testing purposes I randomly delay response between 1 to 5 seconds. In real world example, you should use microseconds, not the seconds.


declare(strict_types=1);

namespace AppBundle\Controller;

use AppBundle\Exception\InfluxException;
use AppBundle\Service\InfluxService;

abstract class AbstractController
{
private $influxService;

public function __construct(
InfluxService $influxService
) {
$this->influxService = $influxService;
}

protected function record(string $controller, string $action): void
{
$start = time();
sleep(rand(1, 5));
$end = time();

try {
$this->influxService->write($end-$start, ['controller' => $controller, 'action' => $action]);
} catch (InfluxException $exception) {
// Do something about if if you wish or just ignore
}
}
}

UserController


declare(strict_types=1);

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/users")
*/
class UserController extends AbstractController
{
/**
* @Method({"GET"})
* @Route("/list")
*/
public function listAction(): Response
{
$this->record('users', 'list');

return new Response('User listed.');
}

/**
* @Method({"GET"})
* @Route("/create")
*/
public function createAction(): Response
{
$this->record('users', 'create');

return new Response('User created.');
}

/**
* @Method({"GET"})
* @Route("/delete")
*/
public function deleteAction(): Response
{
$this->record('users', 'delete');

return new Response('User deleted.');
}
}

AdminController


declare(strict_types=1);

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/admins")
*/
class AdminController extends AbstractController
{
/**
* @Method({"GET"})
* @Route("/create")
*/
public function createAction(): Response
{
$this->record('admins', 'create');

return new Response('Admin created.');
}

/**
* @Method({"GET"})
* @Route("/delete")
*/
public function deleteAction(): Response
{
$this->record('admins', 'delete');

return new Response('Admin deleted.');
}
}

InfluxController


declare(strict_types=1);

namespace AppBundle\Controller;

use AppBundle\Exception\InfluxException;
use AppBundle\Service\InfluxService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/influx")
*/
class InfluxController
{
private $influxService;
private $templating;

public function __construct(
InfluxService $influxService,
EngineInterface $templating
) {
$this->influxService = $influxService;
$this->templating = $templating;
}

/**
* @Method({"GET"})
* @Route("")
*/
public function indexAction(): Response
{
try {
$performance = $this->influxService->result();
} catch (InfluxException $exception) {
$performance = $exception->getMessage();
}

return (new Response())->setContent(
$this->templating->render('AppBundle:influx:index.html.twig', ['performance' => $performance])
);
}
}

index.html.twig


{% extends '::base.html.twig' %}

{% block body %}
{{ dump(performance) }}
{% endblock %}

InfluxException


declare(strict_types=1);

namespace AppBundle\Exception;

use RuntimeException;

class InfluxException extends RuntimeException
{
}

InfluxService


declare(strict_types=1);

namespace AppBundle\Service;

use AppBundle\Exception\InfluxException;
use Exception;
use InfluxDB\Client;
use InfluxDB\Database;
use InfluxDB\Database\RetentionPolicy;
use InfluxDB\Point;

class InfluxService
{
const DATABASE = 'response_stats';
const MEASUREMENT = 'points';
const RETENTION_POLICY = 'autogen';
const DEFAULT_TAGS = ['application' => 'symfony'];

private $client;

public function __construct(Client $client)
{
$this->client = $client;
}

public function result(): iterable
{
try {
$database = $this->getDatabase();

$total = $database->query(sprintf(
'SELECT COUNT("value") FROM "%s"', self::MEASUREMENT
));

$records = $database->query(sprintf(
'SELECT * FROM "%s" ORDER BY "time" DESC LIMIT 10 OFFSET 0', self::MEASUREMENT
));

return [
'total' => $total->getPoints()[0]['count'],
'records' => $records->getPoints(),
];

} catch (Exception $e) {
throw new InfluxException($e->getMessage());
}
}

public function write(int $elapsed, iterable $tags): void
{
try {
$database = $this->getDatabase();

$points = array(
new Point(
self::MEASUREMENT, // Used in FROM clause
$elapsed, // Actual time series value
$tags, // Used in WHERE clause
self::DEFAULT_TAGS, // Used in SELECT clause
exec('date +%s%N') // Actual timestamp of the record
),
);

$database->writePoints($points, Database::PRECISION_NANOSECONDS);
} catch (Exception $e) {
throw new InfluxException($e->getMessage());
}
}

private function getDatabase(): Database
{
$database = $this->client->selectDB(self::DATABASE);

if (!$database->exists()) {
$database->create(new RetentionPolicy(self::DATABASE, self::RETENTION_POLICY, 1, true));
}

return $database;
}
}

Test


Populate InfluxDB


Create and run PHP test script below in terminal.


$endpoints = [
'users/list',
'users/create',
'users/delete',
'admins/create',
'admins/delete',
];

do {
echo shell_exec('curl http://192.168.99.10:8082/app_dev.php/'.$endpoints[array_rand($endpoints)]);
} while (1 === 1);

Database


Let's login and see what we have in database.


$ influx -username inanzzz -password 123123
Connected to http://localhost:8086 version 1.5.1
InfluxDB shell version: 1.5.1
>
> SHOW DATABASES
name: databases
name
----
_internal
response_stats
>
> USE response_stats
Using database response_stats
>
> SHOW MEASUREMENTS
name: measurements
name
----
points
>
> SELECT * FROM points LIMIT 10
name: points
time action application controller value
---- ------ ----------- ---------- -----
1522569761392457485 delete symfony admins 5
1522569765801070159 delete symfony admins 4
1522569771337090535 list symfony users 5
1522569775671952409 list symfony users 4
1522569778064807532 delete symfony admins 2
1522569782668878979 list symfony users 4
1522569785041316946 list symfony users 2
1522569787419202650 delete symfony admins 2
1522569790641263793 create symfony admins 3
1522569791967151595 list symfony users 1
>

Visuals


Instead of going through how Grafana is installed and configured, I will just show you some examples graphs I set up. GUI is accessible through http://192.168.99.20:3000 and the login credentials is admin:admin.