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?

  1. 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.
  2. 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.
  3. 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:

  1. Acquisizione dei log da file
  2. Filtraggio dei messaggi rilevanti
  3. Parsing e normalizzazione dei dati
  4. 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]
	  )
	)

Grafana SIP calls

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]
  )
)

Grafana Call closed