Contexte : Analyse de log

Quelque soit le format de vos logs, vous pouvez être amenés à mettre en évidence une certaine périodicité chronologique dans vos données. Celle là peut concerner l’utilisation CPU comme le nombre de requête émises etc. La page qui suit propose R comme outil d’analyse. Quelques méthodes de lecture de données sont proposées. Une fois les données lues, elles sont stockées dans un objet de type data.frame de R que nous allons analyser en utilisant la transformée de Fourrier.

Outil proposé : R

Présentation

R est un outil d’analyse de données.

Il est assez simple d’utilisation et est basé sur une librairie de packages fournis majoritairement par la communauté.

Site web

https://cran.r-project.org/

Installation

R peut être installé sous Linux, Windows et MacOS.

Il existe un IDE pratique pour coder, visualiser les graphiques, gérer les packages etc. Il s’appelle RStudio et est téléchargeable sur ce lien: https://www.rstudio.com/

Lecture des données

Il existe plusieurs façon de lire les données de logs avec R. Nous vous en proposons 2. Les 2 méthodes supposent une structures en “colonne” en entrée. Pour plus d’informations: https://cran.r-project.org/doc/manuals/r-release/R-data.pdf

Fichier d’entrée

Supposons que les logs sont stockés dans un fichier “log.dat”, la méthode read.table de R va permettre de créer un tableau de données, que nous appellerons data avec le contenu de ce log. Exemple:

data <- read.table("log.dat")

Celà suppose des colonnes séparées par des espaces. Pour toute “customization” de la lecture, voir les paramètres de cette méthode :https://stat.ethz.ch/R-manual/R-devel/library/utils/html/read.table.html.

La “customization” inclue le chemin vers le fichier, la présence ou non d’entête, le caractère de séparation entre les colonnes, l’encoding etc.

Base de donnée Cassandra

Soit une table Cassandra qui s’appelle “messages” qui se trouve dans un keyspace qui s’appelle “leuville”. Avant de commencer vous devez télécharger le driver jdbc: cassandra-jdbc-2.1.1.jar (http://www.java2s.com/Code/Jar/c/Downloadcassandrajdbc125jar.htm), mais aussi:

  • apache-cassandra-clientutil-1.2.6.jar
  • apache-cassandra-thrift-1.2.6.jar
  • cassandra-all-1.2.9.jar
  • guava-15.0.jar
  • jackson-core-asl-1.9.2.jar
  • jackson-mapper-asl-1.9.2.jar
  • libthrift-0.7.0.jar
  • log4j-1.2.15.jar
  • slf4j-api-1.5.2.jar
  • slf4j-log4j12-1.5.2.jar
  • slf4j-simple-1.5.2.jar

Lister les .jar dans la librairie cassandra jdbc

libraryFiles <- list.files("/cheminVersLaLibrairie/cassandra-jdbc-2.1.1", pattern="jar$", full.names=T)

Utiliser la librairie et le driver cassandra jdbc pour créer le driver Cassandra

cassdrv <- JDBC("org.apache.cassandra.cql.jdbc.CassandraDriver", libraryFiles)

Lancer une connexion sur la base Cassandra et le keyspace ‘leuville’

casscon <- dbConnect(cassdrv, "jdbc:cassandra://adresseIP:portCassandra/leuville")

Recuperer les colonnes voulues de la table “messages” et les stocker dans “dataAll”

dataAll <- dbGetQuery(casscon, "select application_id, uuid, direction, client_ip, data, blobAsBigint(timestampAsBlob(datetime)), headers, host, hostname, ip, method, parameters, status_code, uri from messages")

Renommer les colonnes comme souhaité

colnames(dataAll) <- c("application_id", "uuid", "direction", "client_ip", "data", "datetime", "headers", "host", "hostname", "ip", "method", "parameters", "status_code", "uri")

Pour la suite de l’explication nous parlerons de x, et y comme données. x sera la variable temporelle et y les données à analyser. Dans l’exemple précédent x serait dataAll$datetime et y par exemple dataAll$data.

Proposition d’une méthode d’analyse basée sur la transformée de Fourrier

Le principe de la transformée de Fourrier est de décomposer toute fonction en une somme de fonctions périodiques. Pour plus d’informations sur le principe http://aalem.free.fr/maths/C12-TRANSFORMEE-DE-FOURIER.PDF.

Il existe aussi une version discrète de la transformée de Fourrier. Celle-ci est mieux adaptée à notre cas vu que toutes données réelles ont un échantillonage fini et discret (contrairement à par exemple une fonction mathématique). Par contre une des contraintes de la transformée de Fourrier est que cet échantillonage soit constant! Si ce n’est pas le cas des données, il est possible de suréchantilloné celui là par interpolation.

Enfin les différents langages et outils d’analyse de donnée proposent des version “rapides” de la transformée de Fourrier : Fast Fourrier Transform que nous désignerons par “FFT” pour la suite.

R propose une implémentation de la FFT.

Ci-dessous les 2 fonctions qui vont nous servir dans la suite du travail. Elles sont proposées par ce tutoriel (à lire pour plus d’informations) http://www.di.fc.ul.pt/~jpn/r/fourier/fourier.html. Elles font appel à la fonction FFT de R.

La première fonction retourne l’axe des abcisses dans l’espace des fréquences. En effet, la FFT va nous permettre de passer les données de l’espace “réel” à celui des fréquences avec une amplitude comme ordonnée. Dans cet espace un pic correspond à une fréqunence et donc une période et donc une périodicité.

getFFTFreqs <- function(Nyq.Freq, data)
{
  if ((length(data) %% 2) == 1) # Odd number of samples
  {
    FFTFreqs <- c(seq(0, Nyq.Freq, length.out=(length(data)+1)/2),
                  seq(-Nyq.Freq, 0, length.out=(length(data)-1)/2))
  }
  else # Even number
  {
    FFTFreqs <- c(seq(0, Nyq.Freq, length.out=length(data)/2),
                  seq(-Nyq.Freq, 0, length.out=length(data)/2))
  }

  return (FFTFreqs)
}

Cette fonction va être utilisée par la suivante. La fonction suivante permet de calculer la FFT et de tracer le résultat dans un plot “Amplitude” en fonction des “Fréquences”.

Présentation des paramètres:

  • x,y -> les données en entrée. x est dans notre exemple le temps
  • samplingFreq -> l’échantillonage en fréquences
  • shadeNyq -> paramètre graphique laisser à TRUE

Présentation des retours

  • freq -> les fréquences
  • FFT -> les valeurs de la FFT
  • modFFT -> l’amplitude ou le “modulus” de la FFT
plotFFT <- function(x, y, samplingFreq, shadeNyq=TRUE)
{
  Nyq.Freq <- samplingFreq/2
  FFTFreqs <- getFFTFreqs(Nyq.Freq, y)

  FFT <- fft(y)
  modFFT <- Mod(FFT)
  FFTdata <- cbind(FFTFreqs, modFFT)
  plot(FFTdata[1:nrow(FFTdata)/2,], t="h", pch=20, lwd=2, cex=0.8, main="",
       xlab="Frequency (Hz)", ylab="Power")
  if (shadeNyq == TRUE)
  {
    # Gray out lower frequencies
    rect(0, 0, 2/max(x), max(FFTdata[,2])*2, col="gray", density=30)
  }

  ret <- list("freq"=FFTFreqs, "FFT"=FFT, "modFFT"=modFFT)
  return (ret)
}

Vous n’êtes pas obligés de comprendre les détails de ces fonctions pour savoir les utiliser. Voici un exemple d’utilisation.

Soit des données x, yx est le temps et échantillonage de 0.01 sec.

delta <- 0.01

image On lance le calcul de la FFT comme suit:

res <- as.data.frame(plotFFT(x, y, 1/delta), stringAsFactor = FALSE)

image

res contient le résultat de ce calcul. Nous pouvons détecter déjà un pic de fréquence qui correspond à une périodicité. Pour détecter les pics de fréquences automatiquement, nous allons utiliser une approche de traitement de signal. Nous allons supposer que dans cet espace des fréquences, le bruit est représenté en gros par la médiane des amplitudes, et que tout ce qui est plus haut que médiane plus 5 fois la dispersion est considéré comme un vrai signal et donc un pic (et donc une période et donc une périodicité).

Sur le graphique précédent la ligne rouge correspond à la médiane et la ligne bleue à la limite “médiane plus 5 fois la dispersion”. Elles sont tracées avec le code qui suit:

leng <- length(res$freq)
m <- median(res$modFFT)
sd <- sd(res$modFFT)

#Tracer la droite de la médiane
l <- as.data.frame(rep(m, leng))
colnames(l) <- c("median")
lines(res$freq, l$median, col="red")

#Tracer la droite des 5*sigma
l <- as.data.frame(rep(m+5*sd, leng))
colnames(l) <- c("median_5sigma")
lines(res$freq, l$median_5sigma, col="blue")

Reste maintenant à déterminer automatiquement ces pics et les périodes qui leur correspondent grace aux 2 méthodes suivantes.

Noter que nous excluons un pic à zéro car il correpond à une périodicité infinie qui n’a pas de sens et qui est toujours présente.

detectPeak <- function(res, sigmaCoef, excludeZero = TRUE){
  if (excludeZero){
    res$modFFT[1] <- 0
  }
  leng <- length(res$freq)
  m <- median(res$modFFT)
  sd <- sd(res$modFFT)

  l <- as.data.frame(rep(m, leng))
  colnames(l) <- c("median")
  lines(res$freq, l$median, col="red")
  #Tracer la droite des sigmaCoef*sigma
  l <- as.data.frame(rep(m+sigmaCoef*sd, leng))
  colnames(l) <- c("median_5sigma")
  lines(res$freq, l$median_5sigma, col="blue")

  peaks <- subset(res, res$modFFT > m+sigmaCoef*sd)
  peaks <- subset(peaks, peaks$freq > 0)
  print(peaks)

  if(length(peaks$freq) == 0){
    return ("No periodicity detected")
  }else{
    peak <- subset(peaks, peaks$modFFT == max(peaks$modFFT))
    return(peak$freq)
  }
}

La période est l’inverse de la fréquence des pics. Il peut bien sûr y avoir zéro ou plusieurs périodes dans nos données.

detectPeriod <- function(peak){
  if( class(peak) == "character"){
    return(peak)
  }else{
    period <- 1 / peak
    return(period)
  }

}

Appel des 2 fonctions.

peak <- detectPeak(res, 5)
period <- detectPeriod(peak)
print(period)

Si elle(s) existe(nt) nous auront la période des données en entrée.