Monitorare traffico SIP su Kamailio usando Vector, Loki e Grafana
Uno degli aspetti più importanti quando si gestisce traffico VoIP, ma in generale per qualsiasi tipo di sistema che eroga servizi, è quello di capire cosa sta succedendo ed avere la possibilità di monitorare l’andamento del traffico in tempo reale, controllando i flussi di traffico gestiti, la distribuzione del carico sui vari sistemi, e gli eventuali casi di errore. Per lo scenario specifico caratterizzato da un SIP server Kamailio, l’uso di una observability chain realizzata usando Vector, Loki e Grafana è secondo me una buona soluzione di controllo del traffico.
Ma che cosa fanno esattamente Vector, Loki e Grafana e come possono aiutarci con Kamailio?
- Vector: permette di creare una observability pipeline per raccogliere, trasformare e inoltrare dati di osservabilità come log, metriche o eventi, da varie sorgenti e verso diverse destinazioni; nel nostro caso la sorgente è il log di Kamailio e la destinazione è Loki. Si caratterizza per l’estrema flessibilità, alte performance e limitato impegno di risorse di calcolo.
- Loki: è un sistema di aggregazione ed indicizzazione dei log sviluppato da Grafana Labs; è estremamente efficiente perché indicizza principalmente label e non il contenuto completo dei log, consentendo ricerche veloci.
- Grafana: è l’interfaccia grafica che permette di visualizzare i dati provenienti da diverse sorgenti (nel nostro caso Loki), con la possibilità di creare grafici e dashboard per l’analisi dei dati provenienti dalla sorgente, trasformati da Vector ed indicizzati da Loki.
L’idea di base è quella di configurare Kamailio affinché generi messaggi di log strutturati e allineati al call flow SIP; quindi attivare Vector per processare questi log, trasformarli ed inviarli a Loki aggiungendo le label utili per indicizzazione e query; creare grafici e dashboard su Grafana sfruttando l’indicizzazione di Loki.
Struttura log Kamailio
Nel mio scenario Kamailio (configurato per scrivere log in formato JSON) lavora come un proxy statefull, quindi resta nel path di chiamata per tutta la durata del dialogo, e l’obiettivo è quello di scrivere righe di log strutturate per ogni tipologia di messaggi: Requests ricevute (es. INVITE, BYE, CANCEL), Requests inoltrate, Replies ricevute (es. 180 Ringing, 200 OK, codici di errore), Replies inoltrate. Tutto ciò può essere gestito all’interno della logica applicativa di Kamailio tenendo conto di come sono gestite Requests e Responses.
- SIP Requests ricevute: sono intercettate nel blocco request_route e gestite nei blocchi route {…}
- SIP Response ricevute: sono intercettate nel blocco reply_route e gestite in onreply_route[MANAGE_REPLY]
- SIP Requests inviate: sono intercettate e gestite nel blocco onsend_route
- SIP Responses inviate: sono anche loro gestite nel blocco onsend_route, ma si deve abilitare il parametro globale onsend_route_reply=yes
A queste quattro tipologie di messaggi, nella logica applicativa che trovate per intero qui kamailio.cfg, ho fatto corrispondere le seguenti righe di log, tutte di livello INFO e tutte che riportano nel parametro event questi valori:
- rx-request
- rx_reply
- tx_request
- tx_reply
In particolare, quando arriva una SIP Request la riga di log prodotta e corredata di tutta una serie di variabili è questa:
xlog("L_INFO","INFO: event=rx_request calldirection=$dlg_var(calldir) srcip=$si srcport=$sp callid=$ci ruri=$ru method=$rm respcode=$rs respmess=$rr cseq=$cs fromuser=$fU fromtag=$ft touser=$tU totag=$tt dsturi=$du callend=$dlg_var(callend) label='t_relay to Media Server'\n");
Se arriva una SIP Response viene prodotta questa riga:
xlog("L_INFO", "INFO: event=rx_reply calldirection=$dlg_var(calldir) srcip=$si srcport=$sp callid=$ci ruri=$ru method=$rm respcode=$rs respmess=$rr cseq=$cs fromuser=$fU fromtag=$ft touser=$tU totag=$tt dsturi=$dlg_var(dsturi) callend=$dlg_var(callend) label=''\n");
Invece nel caso di Request o Response trasmesse da Kamailio la gestione presente nel blocco onsend_route è la seguente:
if ($rs != $null) {
xlog("L_INFO", "INFO: event=tx_reply calldirection=$dlg_var(calldir) srcip=$si srcport=$sp callid=$ci ruri=$ru method=$rm respcode=$rs respmess=$rr cseq=$cs fromuser=$fU fromtag=$ft touser=$tU totag=$tt dsturi=$dlg_var(dsturi) callend=$dlg_var(callend) label='outgoing reply to $sndto(ip):$sndto(port)'\n");
} else {
xlog("L_INFO", "INFO: event=tx_request calldirection=$dlg_var(calldir) srcip=$si srcport=$sp callid=$ci ruri=$ru method=$rm respcode=$rs respmess=$rr cseq=$cs fromuser=$fU fromtag=$ft touser=$tU totag=$tt dsturi=$dlg_var(dsturi) callend=$dlg_var(callend) label='outgoing request to $sndto(ip):$sndto(port)'\n");
}
In questo modo i log prodotti hanno una struttura chiara e ben organizzata, pronti per essere processati da Vector e passati a Loki.
Vector pipeline
Vector trasforma i log prodotti da Kamailio in informazioni utili per il monitoraggio e l’analisi delle chiamate, estrae le informazioni rilevanti dei messaggi SIP, elimina il rumore operativo e invia gli eventi strutturati a Loki per la successiva consultazione tramite Grafana. La pipeline disponibile qui vector.yaml è composta da quattro fasi principali:
- Acquisizione dei log da file
- Filtraggio dei messaggi rilevanti
- Parsing e normalizzazione dei dati
- Invio a Loki
Acquisizione dei log
La sorgente legge il file di log di Kamailio.
sources:
kamailio_file:
type: file
include: - /var/log/kamailio.log
read_from: end
L’opzione read_from: end fa sì che Vector inizi a leggere soltanto i nuovi eventi generati dopo il suo avvio, evitando di elaborare lo storico già presente nel file.
Filtraggio delle informazioni utili
Non tutti i messaggi scritti da Kamailio sono utili per l’analisi delle chiamate, ed il filtro iniziale seleziona esclusivamente i messaggi generati dal processo Kamailio, gli eventi provenienti dal modulo xlog, i messaggi di livello INFO, escludendo i log prodotti da RTPENGINE. In questo modo vengono eliminati numerosi eventi che non contribuiscono all’analisi del signaling SIP.
Parsing e normalizzazione
La fase centrale della pipeline è il transform remap, che converte il log testuale in un evento strutturato. Vengono estratti il timestamp ed il payload JSON generato da Kamailio; quindi, si inizializzano i campi che verranno utilizzati successivamente in modo che ogni record inviato a Loki abbia sempre la stessa struttura, facilitando le query e la costruzione di dashboard Grafana. La pipeline inoltre scarta gli ACK locali assorbiti da Kamailio e le Requests di tipo Re-INVITE, controllando il Cseq. In questo modo viene effettuato il parsing di stringhe strutturate ricavando tutti gli attributi necessari per un’analisi SIP.
event=rx_request
calldirection=inbound
srcip=...
callid=...
method=INVITE
respcode=200
...
Invio a Loki
L’ultima fase della pipeline invia gli eventi a Loki.
sinks:
loki:
type: loki
Gli eventi vengono serializzati in JSON e corredati da una serie di label Loki che consentono interrogazioni estremamente rapide da Grafana. Ad esempio è possibile filtrare:
- tutte le INVITE ricevute;
- tutte le risposte 4xx;
- il traffico verso uno specifico sistema;
- le chiamate terminate con determinati codici SIP.
Setup Loki e Grafana
Loki e Grafana sono avviati come container; Loki riceve i dati da Vector e li archivia, mentre Grafana si collega a Loki per interrogare i dati tramite query, trasformandoli in dashboard, grafici e report facilmente consultabili.
version: "3.9"
services:
loki:
image: grafana/loki:latest
container_name: loki
restart: unless-stopped
command:
- -config.file=/etc/loki/local-config.yaml
ports:
- "3100:3100"
volumes:
- ./loki/loki-config.yaml:/etc/loki/local-config.yaml
- ./loki/data:/loki
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
depends_on:
- loki
ports:
- "3000:3000"
volumes:
- ./grafana/data:/var/lib/grafana
Eseguire Query LogQL e Dashboard
Dall’interfaccia web di Grafana possiamo effettuare query, estrarre dati e creare grafici e dashboard. Per esempio, con la pipeline di observability descritta in questo post possiamo visualizzare le chiamate ricevute e le chiamate andate in connessione usando le query descritte di seguito.
sum(
count_over_time(
{app="kamailio",event="rx_request",method="INVITE"}[1m]
)
)
sum(
count_over_time(
{app="kamailio",event="rx_reply",method="INVITE",respcode="200"}[1m]
)
)
sum(
count_over_time(
{app="kamailio",event="rx_request",method="ACK"}[1m]
)
)
sum(
count_over_time(
{app="kamailio",event="rx_request",method="CANCEL"}[1m]
)
)

Oppure visualizzare le chiamate chiuse dal caller e le chiamate chiuse dal called, usando queste query.
sum(
count_over_time(
{app="kamailio",event="rx_request",method="BYE",callend="caller"}[1m]
)
)
sum(
count_over_time(
{app="kamailio",event="rx_reply",method="BYE",respcode="200",callend="caller"}[1m]
)
)
sum(
count_over_time(
{app="kamailio",event="rx_request",method="BYE",callend="called"}[1m]
)
)
sum(
count_over_time(
{app="kamailio",event="rx_reply",method="BYE",respcode="200",callend="called"}[1m]
)
)
