Обзор языка R на примере анализа логов

Опубликовано:

"R — язык программирования для статистической обработки данных и работы с графикой... R широко используется как статистическое программное обеспечение для анализа данных и фактически стал стандартом для статистических программ." из Википедии

Решил немного автоматизировать часть своей работы, связанную с анализом данных из логов приложений. Выбор пал на R, и по большей части остался удовлетворен. Теперь есть мысли задействовать этот инструмент и для других целей, где требуется анализ данных. В честь этого набросал небольшой обзор базовых возможностей языка R на примере анализа логов. Но не используйте эту статью как руководство, или тем более как best practices, все ниже изложенное, не более чем попытка дать общее представление о базовых возможностях R (но критика и замечания приветствуются).

Импорт и подготовка данных к анализу

Прежде чем приступить к анализу данных, эти данные необходимо импортировать и привести к удобному виду. В R самый простой способ сделать это, это импортировать данные из csv формата. Access лог, зачастую, вполне пригоден для этого - если в качестве разделителя использовать символ пробела.

# Часто используемый формат логов в NGINX:
# '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent '
# '"$http_referer" "$http_user_agent" $request_time $upstream_response_time $pipe'
# 127.0.0.1 - - [16/Nov/2016:18:00:41 +0100] "GET / HTTP/1.1" 200 1000 "http://example.com/" "Mozilla/5.0..." 0.044 0.036 .

# Подключаем пакет data.table
library(data.table)

# Импортируем данные из лога в переменную 'access_log'
access_log <- fread('/var/log/nginx/access.log',
    # Все текстовые столбцы рассматриваем как текстовый вектор, а не Фактор
    stringsAsFactors = FALSE,
    # Задаем пробел в качестве разделителя
    sep = " ",
    # Заголовки столбцов отсутствуют в импортируемом файле
    header = FALSE,
    # Задаем символ "-" в качестве признака отсуствия данных (NA)
    na.strings = "-",
    # Задаем имена столбцов вручную (не обязательно, но так удобней оперировать данными)
    col.names = c('remote_addr', '_', 'remote_user', 'time', 'tz', 'request', 'status', 'body_bytes_sent',
                                'http_referer', 'http_user_agent', 'request_time', 'upstream_response_time', 'pipe'),
    # Задаем типы данных в каждом столбце
    colClasses = c("character", "character", "character", "character", "character", "character", "numeric", "numeric",
                                                        "character", "character", "numeric", "numeric", "character")
)
# Проверим что у нас получилось:
head(access_log, n = 1)

   remote_addr  _ remote_user                  time     tz        request status body_bytes_sent        http_referer http_user_agent request_time upstream_response_time pipe
1:   127.0.0.1 NA          NA [14/Nov/2016:06:48:57 +0100] GET / HTTP/1.1    200            1000 http://example.com/  Mozilla/5.0...        0.031                  0.031    .

Видим 2 проблемы в данных:

  1. Время запроса разбито на 2 столбца: 'time' и 'tz'
  2. Столбец 'request' содержит в себе 'HTTP METHOD', 'PATH' и 'PROTOCOL' в одной строке

Исправим это:

library(dplyr)
library(tidyr)

# Устанавливаем нужную локаль для корректной конвертации даты
Sys.setlocale("LC_TIME", "en_US.UTF-8") # Linux
# Sys.setlocale("LC_TIME", "English") # Windows

access_log <- access_log %>%
  # Объединяем 2 столбца '[16/Nov/2016:18:00:41', '+0100]' в один, с именем 'ts'
  unite(ts, time, tz, sep="") %>%
  # и приводим к стандартом для R типу даты
  mutate(ts = as.POSIXct(ts, format = "[%d/%b/%Y:%H:%M:%S%z]", tz='GMT')) %>%
  # разбиваем столбец request на 3 столбца: 'request_method', 'request_url_path', 'request_protocol'
  separate(request, c("request_method", "request_url_path", "request_protocol"), sep=' ')
# Проверяем результат:
head(access_log, n = 1)
    remote_addr  _ remote_user                  ts request_method request_url_path request_protocol status body_bytes_sent        http_referer
1 95.90.228.155 NA          NA 2016-11-14 05:48:57            GET                /         HTTP/1.1    200            1000 http://example.com/
  http_user_agent request_time upstream_response_time pipe
1  Mozilla/5.0...        0.031                  0.031    .

На этом можно считать что данные готовы к дальнейшему анализу. Но нужно сделать несколько важных замечаний.

Big Data

Стандартные библиотеки R, импортируют и манипулируют данными в оперативной памяти, и в случае с большими логами (и вообще - в случае Big Data) - это создает проблемы. Решением может быть к примеру:

  • Уменьшить объем данных: предварительная агрегация или отсеивание данных за пределами R
  • Использовать базы данных, и в R, запрашивать данные из базы, перекладывая часть работы по агрегация или выборки на БД
  • Использовать библиотеки и связанные технологии, позволяющие работать с большими данными. К примеру пакет ff позволяет держать данные на жестком диске, вместо оперативной памяти

R for Big Data Оригинал схемы

Неудобный формат логов для импорта

Я специально выбрал для примеров Access лог, потому что он удачно подходит для импорта в виде csv. Но зачастую формат логов непригоден для импорта в таком виде. Для решения этой проблемы можно, к примеру, читать данные из лога построчно, парсить, и приводить к нужным структурам прямо из R. Но мне такой способ показался неудобным, и долгим. Оптимальным для себя вариантом я выбрал - конвертацию логов в csv перед импортом в R. Заодно это позволяет уменьшить объем данных, и соответственно уменьшить требования к оперативной памяти.

В большинстве случаев, конвертировать в csv, можно с помощью sed и регулярок:

# Пример интересующей строки в логе: "2016-11-16T05:53:51+0100 DEBUG: Curl requests time: 59 ms"
-> % cat application.log | sed -rn 's/^([^[:blank:]]+) DEBUG: Curl requests time: ([0-9]+) ms/"\1";\2/p'
"2016-11-16T05:53:51+0100";59
"2016-11-16T05:53:55+0100";43
"2016-11-16T05:53:56+0100";24
...

Если лог содержит json строки, то можно использовать jq

# Пример интересующей строки в логе:
# "2016-11-16T05:53:51+0100 DEBUG: Request: {"param1":21,"param2":{"param3":"value"}}"

# Сперва конвертируем интересующую нас информацию в json
-> % cat application.log | sed -rn 's/([^[:blank:]]+) DEBUG: Request: (\{.*\})$/{"date":"\1","data":\2}/p'
{"date":"2016-11-16T05:53:51+0100","data":{"param1":21,"param2":{"param3":"value"}}}
...

# Далее конвертируем json в csv
-> % cat application.log.json | jq -r '[.date, .data.param1, .data.param2.param3] | @csv'
"2016-11-16T05:53:51+0100",21,"value"
...

Анализ данных с помощью R

Импорт и приведение данных к удобному виду, пожалуй, наиболее сложная задача. Для анализа же используются штатные средства, предоставляемы пакетами R. Далее будет несколько довольно простых примеров использования R для анализа данных, которые мы импортировали в переменную access_log.

library(lubridate)

# суммарная статистика c группировкой в 15 минут:
access_log.15min <- access_log %>%
  # добавляем столбец 'interval', по которому будем группировать данные
  mutate(interval = floor_date(ts, unit="hour") + minutes(floor(minute(ts)/15)*15)) %>%
  # группируем
  group_by(interval) %>%
  # Подсчитываем кол-во запросов за интервал времени и время запроса (максимальное, минимальное, среднее, медиану)
  summarise(
    requests = n(),
    avg_request_time = mean(request_time, na.rm = TRUE),
    max_request_time = max(request_time, na.rm = TRUE),
    min_request_time = min(request_time, na.rm = TRUE),
    med_request_time = median(request_time, na.rm = TRUE)
  ) %>%
  # Выбираем только интересующие нас столбцы
  select(interval, requests, avg_request_time, max_request_time, min_request_time, med_request_time)
# смотрим результат
head(access_log.15min, n = 2)

# A tibble: 1 × 6
             interval requests avg_request_time max_request_time min_request_time med_request_time
               <dttm>    <int>            <dbl>            <dbl>            <dbl>            <dbl>
1 2016-11-14 05:45:00    10000        0.0428198            9.983                0            0.035
2 2016-11-14 05:50:00    57890        0.0431234            8.389                0            0.032

Часто бывает сложно анализировать информацию в табличном виде. На помощь приходит графическое представление данных:

library(ggplot2)

ggplot(access_log.15min, aes(x = interval, y = requests )) + geom_line()

ggplot(access_log.15min, aes(x = interval, y = avg_request_time)) + geom_line()
ggplot ggplot

Со сбором и визуализацией подобной статистики неплохо справляются системы мониторинга, поэтому немного усложню: допустим нас интересует статистика по определенным запросам, или группам запросов. Создадим функцию, которая будет задавать категории запросам по request_url_path:

request_type <- function (paths) {
  ret <- vector(mode="character", length=length(paths))
  ret[startsWith(paths, '/blog')] = 'category1'
  ret[startsWith(paths, '/news')] = 'category2'
  ret[startsWith(paths, '/articles')] = 'category3'
  return(ret)
}

Сгруппируем данные также с 15 минутным интервалом, но еще и по категории запроса

access_log.request_type.15min <- access_log %>%
  mutate(
    request_type = request_type(request_url_path),
    interval = floor_date(ts, unit="hour") + minutes(floor(minute(ts)/15)*15)
  ) %>%
  group_by(request_type, interval) %>%
  summarise(
    requests = n(),
    avg_request_time = mean(request_time, na.rm = TRUE),
    max_request_time = max(request_time, na.rm = TRUE),
    min_request_time = min(request_time, na.rm = TRUE),
    med_request_time = median(request_time, na.rm = TRUE)
  ) %>%
  select(request_type, interval, requests, avg_request_time, max_request_time, min_request_time, med_request_time) %>%
  filter(nchar(request_type) > 0)
head(access_log.request_type.15min, n = 3)

Source: local data frame [3 x 7]
Groups: request_type [3]

  request_type            interval requests avg_request_time max_request_time min_request_time med_request_time
         <chr>              <dttm>    <int>            <dbl>            <dbl>            <dbl>            <dbl>
1    category1 2016-11-14 05:45:00     8577       0.03686359            0.138            0.017           0.0340
2    category2 2016-11-14 05:45:00     1352       0.07956583            9.983            0.000           0.0480
3    category3 2016-11-14 05:45:00       20       0.10605000            0.559            0.031           0.0855
ggplot(access_log.request_type.15min, aes(x = interval, y = requests, col = request_type)) + geom_line()
ggplot(access_log.request_type.15min, aes(x = interval, y = avg_request_time, col = request_type)) + geom_line()
ggplot ggplot

В примерах выше я использовал пакет dplyr для работы с данными. Но он не единственный, есть к примеру такая:

library(sqldf)
sqldf('SELECT request_type, SUM(requests), AVG(avg_request_time) FROM `access_log.request_type.15min` GROUP BY request_type')

  request_type SUM(requests) AVG(avg_request_time)
1    category1          8577            0.03686359
2    category2          1352            0.07956583
3    category3            20            0.10605000

Не всегда логи содержат всю необходимую информацию, часть данных может находиться в базах, и в R есть средства, позволяющие импортировать данные из других источников данных, проводить объединения таблиц - LEFT, RIGHT, INNER, OUTER JOIN и т.д.

Отчеты

И напоследок пример того, как можно сгенерировать отчет из R. Создадим шаблон отчета в формате Markdown

---
title: "Access Log Report Example"
---
Date: `r format(Sys.time(), "%Y-%m-%d %H:%M:%S")`

Number of lines : `r nrow(access_log)`

Start Date in Log: `r format(min(access_log$ts), "%Y-%m-%d %H:%M:%S")`

End Date in Log: `r format(max(access_log$ts), "%Y-%m-%d %H:%M:%S")`

# Request time per hour:
```{r, echo=FALSE}
access_log.1h <- access_log %>%
    mutate(interval = floor_date(ts, unit = "hour")) %>%
    group_by(interval) %>%
    summarise(
        requests = n(),
        avg = mean(request_time, na.rm = TRUE),
        max = max(request_time, na.rm = TRUE),
        min = min(request_time, na.rm = TRUE)
    ) %>%
select(interval, requests, min, max, avg)

library(pander)
panderOptions("digits", 2)
pander(access_log.1h)
```

# Average request time per 15 min:
```{r, echo=FALSE}
ggplot(access_log.15min, aes(x = interval, y = avg_request_time)) + geom_line()
```

# Count of requests per 15 min for each category:
```{r, echo=FALSE}
ggplot(access_log.request_type.15min, aes(x = interval, y = requests, col = request_type)) + geom_line()
```

И сгенерируем отчет, к примеру, в формате pdf

library('rmarkdown')
render("markdown.rmd", pdf_document(), output_file = "report.pdf")

Получившийся отчет можно посмотреть тут.

Комментарии

Комментарии остутствуют

Оставить комментарий


Комментарии перед публикацией проверяются