Обзор языка 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")

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

R, Анализ данных

Комментарии

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