Appendix I: Manual service installation

While the section below explains how data export, data splitting and automatic configuration work, by showing how these services are configured, your life will be easier if you designate a small VM separate from the main PBX to do these jobs, and then use the Ansible scripts we provide to run and manage all these tasks for you. See the Quick-start Guide.

Setting up the services

We will be creating two services on the FusionPBX box; one of them uniloader-freeswitch downloads data continuously from FreeSwitch and generates a queue_log file; and the other one uniloader-splitter knows which tenants are active (based on a file you specify) and uploads data to all of them.

All commands below need to be run as root.

Installing Uniloader

We will install Uniloader as a symlink under /opt/uniloader, so that it is always in the same place even when it is upgraded. We will also add it to the system path so that it’s available from any shell.

The link for the current version of Uniloader is available from https://www.queuemetrics.com/download.jsp

mkdir -p /opt/uniloader
cd /opt/uniloader
wget https://downloads.loway.ch/qm/uniloader-25.09.1.tar.gz
tar zxvf uniloader-25.09.1.tar.gz
ln -s ./uniloader-25.09.1/bin/uniloader_amd64 uniloader
ln -s /opt/uniloader/uniloader /usr/bin/uniloader

Now we create a folder to store queue_log data into:

mkdir -p /opt/uniloader/qlogs

Sanity checks

In order to continue, you will need accesses to your FusionPBX / FreeSwitch system. Uniloader includes ways to test and debug connections, so we will use it to assert everything is okay.

Checking ESL access to FreeSwitch

You can make sure that local access to FreeSwitch’s ESL port is allowed by typing:

# uniloader test fsw-esl --host "127.0.0.1" --port "8021" --auth "ClueCon"
2020/02/04 06:43:19 Testing Freeswitch connection to '127.0.0.1:8021' with auth token 'ClueCon'

2020/02/04 06:43:19 <ESL: Content-Type: command/reply
2020/02/04 06:43:19 <ESL: Reply-Text: +OK accepted
2020/02/04 06:43:19 <ESL:
2020/02/04 06:43:19  ======= Login OK
....

If you see the Login OK message, then you are set. If not, you will have to check ESL access credentials and allowed IPs.

Checking access to FusionPBX’s database

FusionPBX generates a random password for the database on installation. To find the password for your FusionPBX database, print the contents of your file /etc/fusionpbx/config.php:

//pgsql: database connection information
	$db_host = 'localhost'; //set the host only if the database is not local
	$db_port = '5432';
	$db_name = 'fusionpbx';
	$db_username = 'fusionpbx';
	$db_password = 'UELfQn8gWg6bp6SRMoOhwqZ34F0';

Now test that it’s working by typing:

# uniloader test postgres --ps-uri "localhost/fusionpbx" --ps-login "fusionpbx" --ps-pwd UELfQn8gWg6bp6SRMoOhwqZ34F0
2020/02/04 06:49:04 Testing Postgres connection to 'postgres://fusionpbx:UELfQn8gWg6bp6SRMoOhwqZ34F0@localhost/fusionpbx'

2020/02/04 06:49:04  -- Connection took 13.287µs
2020/02/04 06:49:04  -- Query took 11.365098ms
2020/02/04 06:49:04 Local time on database is: 2020-02-04T06:49:04.033431+01:00

If it prints out the local time, it’s working.

Checking data upload to an instance

We first want to make sure that the QueueMetrics Live instance is available and that our password is correct:

# uniloader test upload --uri  https://stats.pbx.my/tenant1 --pass 12345678
Testing upload credentials.

High Water Mark is 1580741450 [2020-02-03 15:50:50 +0100 CET]
Connection OK

On a brand new system, you will see the High Water Mark set to zero.

Checking automatic configuration of FusionPBX’s agents and queues

We then want to make sure that our tenant is available in FusionPBX:

# uniloader pbxinfo fusionpbx --ps-pwd UELfQn8gWg6bp6SRMoOhwqZ34F0

=  Tenant # 1: tenant1.pbx.my

-- Queues found: 3
   #            Code            Name                       Ext.Ref.
   1           00all          00 All
   2             500            q500 64032aac-f3b2-474b-a2c3-08f1a35ea16a
   3             501            q501 53b66cc6-51b7-43b9-84ef-22d44fc46f69

-- Agents found: 3

   #            Code            Name                       Ext.Ref.
   1       Agent/200          Martin 391770f4-aaad-4834-9050-0770a32782d0
   2       Agent/201           Vince 6d26623c-e5a9-46a1-93b3-937389ca8a02
   3       Agent/202            Dave 64eab4f9-829a-4fb8-8cbc-d2e6a5443fd6

If you see any agents/queues appearing, it means that the tenant will generate data and can be uploaded.

The number of agents you see here is a good guess about the required size of your QueueMetrics Live instance.

Service uniloader-freeswitch: log generation

We will create a systemd service to export data out of the PBX and format it in a way that is compatible with QueueMetrics.

If this service is not active, queue data generated on the switch will be lost. So you should never stop (or even restart) this service while the PBX is running; and if you do, you should do it when there is no traffic on your PBX.

Create a new file /etc/systemd/system/uniloader-freeswitch.service.

#
# Systemd Unit for Uniloader FreeSwitch ESL Export
#

[Unit]
Description=Generates queue_log from ESL events
After=syslog.target network.target freeswitch.service

[Service]
Type=simple
#EnvironmentFile=/etc/uniloader-freeswitch
Nice=15
KillMode=process
PIDFile=/var/run/uniloader-freeswitch.pid
ExecStart=/opt/uniloader/uniloader fsw  --queuelog /opt/uniloader/qlogs/qlog-synth.txt --ps-pwd UELfQn8gWg6bp6SRMoOhwqZ34F0  --shorten-domain "1"
RestartSec=1
Restart=on-failure


[Install]
WantedBy=multi-user.target

We now run:

systemctl daemon-reload
systemctl start  uniloader-freeswitch
systemctl enable uniloader-freeswitch

This way the service is started immediately and will restart on boot, after Freeswitch itself starts.

To check if the system is working correctly, run:

# systemctl status  uniloader-freeswitch
- uniloader-freeswitch.service - Generates queue_log from ESL events
   Loaded: loaded (/etc/systemd/system/uniloader-freeswitch.service; disabled; vendor preset: enabled)
   Active: active (running) since Tue 2024-02-04 07:01:36 CET; 6s ago

Now, if there are any agents defined, their current status will be written on /opt/uniloader/qlogs/qlog-synth.txt. When any call of interest starts, you will see activity.

Filtering outbound calls

You can control which calls should be included in the automatic outbound ones. This lets you exclude specific extensions from tracking (e.g. the CEO’s) or just include some exact routes (e.g. only calls where the callee starts with the digit 0).

Log rotation

While not technically necessary, we suggest setting up weekly or monthly log rotation. As restarts on the uniloader-splitter service will have to do a full rescan of the current file, it would definitely be advisable to keep its size to a manageable amount. Long rescans will cause no data losses, but real-time operations on instances will be delayed.

There is no need to do anything but rename the current queue-log-synth.txt file to a new file. The splitter will finishing uploading the old file before moving ahead to the new file.

The Ansible installer sets up a weekly rotation for you, with 50 weeks of data retention on the server..

Service uniloader-splitter: Setting up data export

We now have to define a service for data export. For now, let’s imagine we have no tenant, so that it has nothing to do. Our service will read data from the queue_log file generated by uniloader-freeswitch, check each entry against a set of rules that specify our known tenants we want to feed, and do nothing because we have none so far.

We first create the rules file; edit a new file under /opt/uniloader/splitter.json that for now just contains two square brackets, as below:

[]

Create a new file /etc/systemd/system/uniloader-splitter.service.

#
# Systemd Unit for Uniloader Splitter
#

[Unit]
Description=Loway Uniloader Splitter
After=syslog.target network.target


[Service]
Type=simple
#EnvironmentFile=/etc/uniloader-splitter
Nice=15
KillMode=process
PIDFile=/var/run/uniloader-splitter.pid
ExecStart=/opt/uniloader/uniloader -s /opt/uniloader/qlogs/qlog-synth.txt  upload --splitter /opt/uniloader/splitter.json  --pid /var/run/uniloader-splitter.pid
RestartSec=1
Restart=on-failure


[Install]
WantedBy=multi-user.target

We now install the service as:

systemctl daemon-reload
systemctl start  uniloader-splitter
systemctl enable uniloader-splitter

To make sure it is working correctly, and see its logs when needed:

# systemctl status uniloader-splitter
● uniloader-splitter.service - Loway Uniloader Splitter
   Loaded: loaded (/etc/systemd/system/uniloader-splitter.service; disabled; vendor preset: enabled)
   Active: active (running) since Tue 2020-02-04 07:19:27 CET; 2s ago
 Main PID: 3590 (uniloader)
    Tasks: 6 (limit: 4915)
   CGroup: /system.slice/uniloader-splitter.service
           └─3590 /opt/uniloader/uniloader -s /opt/uniloader/qlogs/qlog-synth.txt upload --splitter /opt/uniloader/splitter.json --pid /var/run/uni

Feb 04 07:19:27 debian systemd[1]: Started Loway Uniloader Splitter.
Feb 04 07:19:27 debian uniloader[3590]: 2020/02/04 07:19:27 Uniloader 0.6.7 (149-20191004.1255) starting upload [PID 3590].
Feb 04 07:19:27 debian uniloader[3590]: 2020/02/04 07:19:27 Uniloader 0.6.7 (149-20191004.1255) is alive - GR: 3 - Mem: Alloc 321k (Free 0k) Sys 68

Editing the tenant rules

Our splitter file contains multiple JSON object that tell Uniloader how to match log entries to known QueueMetrics Live instances and where to send them. While the queue_log file contains information for all queue activity on the system, you will only be sending out data for tenants you have configured.

In our example:

[
	{
		"uri": "https://stats.pbx.my/tenant1",
		"login": "webqloader",
		"pass": "12345678",
		"token": "",
		"matcher": ["tenant1-"],
		"match": "any",
		"removematch": true,
		"disabled": false,
		"noactions": false,
		"clientname": "tenant1"
	}
]

Make sure that:

  • uri and pass are the ones you checked with QueueMetrics Live

  • matcher is the subdomain you use for your tenant, plus a dash.

  • clientname is a free comment

  • you can set disabled to true if you want to "switch off" data upload for a tenant without deleting it from this file.

When done, just restart the service so that changes are picked up.

systemctl restart uniloader-splitter

While the service is restarted (and even if it stays down for a few seconds), no data is being lost, as data is still being written by the uniloader-freeswitch service and stored safely into the queue_log file. The worst thing that can happen are a few seconds of delay on the wallboards - but data is always computed correctly and will sync automatically.

The splitter file should be backed up after each change.

Checking that everything is working

Try sending a call to a queue on your tenant. Answer the call, then hang it up.

Now log in to your QueueMetrics Live instance; from the Home Page click on "Administrative Tools" → "System Diagnostic Tools" → "Live DB Inspector": you should see a number of records appear on the page.

Pre-configuring a QueueMetrics Live instance

You should now run an automated configuration of the QueueMetrics Live instance:

uniloader pbxinfo --mode syncqm -u https://stats.pbx.my/tenant1 -p 12345678
     fusionpbx -ps-pwd UELfQn8gWg6bp6SRMoOhwqZ34F0 --single-tenant tenant1.pbx.my

This will:

  • Create queues

  • Create agents

  • Set the correct FusionPBX IDs for agents, so they can be controlled through QueueMetrics

You should run this command whenever the configuration changes on FusionPBX - you create new queues or new agents, so everything stays in sync.

Service uniloader-audiovault

Create a new unit file called /etc/systemd/system/uniloader-audiovault.service:

[Unit]
Description=Loway Uniloader  AudioVault
After=syslog.target network.target


[Service]
Type=simple
Nice=15
KillMode=process
ExecStart=/usr/bin/uniloader av serve \
              --bind-to "localhost:4040" --public-url "https://av.server.com" \
              --path '{{ av_path }}' \
              --token "n0b0d4kn0ws" --tenants "/opt/uniloader/audiovault-tenants.json"
RestartSec=1
Restart=on-failure

[Install]
WantedBy=multi-user.target

Then create a brain file called audiovault-tenants.json that defines all tenants and their security token:

[
	{
	  "tenant": "tenant1.server.com",
	  "secret": "x1x1secret",
	  "note":   "Customer A"
	},
	{
	  "tenant": "tenant2.server.com",
	  "secret": "x2x2secret",
	  "note":   "Customer B"
	}
]

AudioVault Configuration

To configure the system, it is necessary to specify an address to which AudioVault will be bound. Typically, a proxy server handling secure connections is placed in front of this address. It is possible to modify the FusionPBX configuration to act as a proxy for AudioVault itself.

For each tenant, a unique secret key is defined for authentication. This key must be entered into the QueueMetrics instance configuration for that tenant, and on QueueMetrics itself.

FusionPBX systems typically store queue recordings organized by tenant, year, month, and day. This configuration needs to be communicated to AudioVault so that it can efficiently locate the files, especially when an external storage is used for recordings and files are periodically moved to this storage. AudioVault can be instructed to search in multiple locations if the recording is not found in the primary specified location.

An example configuration follows:

audiovault: True

av_port: "localhost:4040"
av_public_url: "https://server.com/audiovault"
av_path: "file:/var/lib/freeswitch/recordings/%%TE/archive/%%YY/%%ME/%%DD"
av_token: "CHANGEME"

In this configuration:

  • AudioVault is enabled and installed

  • AudioVault binds in HTTP to port 4040, available only on the same server. A proxy needs to be set up to allow external access, to the address "server.com/audiovault"

  • AudioVault needs to know the public URL of the proxy - in this case https://server.com/audiovault

  • A search path is defined - placeholders will be expanded so that the actual path for a recording made on May 8, 2025 for tenant1 might be /var/lib/freeswitch/recordings/tenant1.server.com/archive/2025/May/08/

  • av_token should be a secret string, but it is not used, as each tenant will have their own secret in parameter av_secret.

HTTPS Proxy configuration for AudioVault

You should make sure that ALL connections to AudioVault happen over an encrypted channel.

Using nginx as an HTTPS proxy

In FusionPBX, find the file /etc/nginx/sites-available/fusionpbx and edit it to add a proxy to local port 4040 when any domain is called with the /audiovault url.

Find the part that says:

server {
        listen [::]:443 ssl;
        listen 443 ssl;"

and before the first 'location' row, add this stanza:

location /audiovault/ {
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Forwarded-Host $host;
	proxy_set_header X-Forwarded-Port $server_port;
	proxy_set_header X-Forwarded-Proto $scheme;
	proxy_set_header X-Forwarded-Server $host;
	proxy_set_header X-Forwarded-Ssl on;
	proxy_set_header X-Forwarded-Webapp /audiovault;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Url-Scheme $scheme;
	rewrite ^/audiovault/(.*)$ /$1 break;
	proxy_pass http://127.0.0.1:4040;
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_read_timeout 240s;
}

Then restart nginx. If you connect to any tenant with your browser, e.g. https://tenant1.server.com/audiovault you should see a welcome page.

Make a backup of the fusionpbx file before editing it.