Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Realtime web logger на MongoDB, часть 2

2 комментария

Продолжим обсуждения логгера. В этой части я хочу поговорить о всяких инфраструктурных изменениях и настройках, которые в последствии позволят нам сделать красивый интерфейс. Речь пойдет в основном об усовершенствовании нашего MVC-приложения, использование DLR, а также про внедрение контролов Telerik.

Referrer – это мало

Посмотрев на те данные что накопились у меня в базе, я пришел к достаточно предсказуемому выводу – поле Referrer недостаточно для того, чтобы отражать то, какой контент (т.е. какую страницу) браузит пользователь. Причина простая – помимо изначального источника, контент также показывают аггрераторы, он попадает в копипаст, ну и конечно им пользуются контентные пираты (чтоб их!!!). Следовательно, Referrer порой содержит нечто невразумительное.

К счастью, решение простое… точнее есть несколько вариантов, но мне нравится вот такой: делаем дополнительный параметр в экшне, и это будет некая “метка”, которую мы определеяем сами. Может это не так удобно делать изначально, но зато потом удобно менеджить.

public ActionResult Log(string source)
{
  ...
  Source = source // соотв. изменение в сущности
  ...
}

Соответственно я изменил сущность, отрефакторив Source --> Referrer для хранения URL источника (если он есть), а Source пустил для нашего тэга.

Парсим UserAgent

Вообще то, что возвращают браузеры в строке UserAgent – это апокалипсис (люблю это слово!) и понятное дело что нужен парсер чтобы иметь вменяемое представление вот этого:

Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.126 Safari/533.4

Вместо того, чтобы писать парсер собственноручно, я нашел говое решение на Python и решил попробовать все прелести DLR для безболезненного внедрения стороннего кода. Скажу сразу: все получилось достаточно просто.

Во-первых, в просто добавил вышеупомянутый файл как embedded resource.

Во-вторых, я подгрузил его в статическом конструкторе контроллера, инициализировав объекты pythonEngine и pythonScope:

pythonEngine = Python.CreateEngine();
pythonScope = pythonEngine.CreateScope();
using (var s = Assembly.GetExecutingAssembly().GetManifestResourceStream(
  "VisitLogger.Scripts.UserAgentParser.py"))
{
  using (var sr = new StreamReader(s))
  {
    var code = sr.ReadToEnd();
    var source = pythonEngine.CreateScriptSourceFromString(code);
    source.Execute(pythonScope);
  }
}

В момент когда нам нужно распарсить строку, мы просто используем pythonEngine для того чтобы вызвать функцию и получить из нее PythonTuple с четырьмя элементами – названием браузера и тремя элементами его версии (версии могут быть равны None). А дальше все просто – собираем строку и возвращаем.

public string GetBrowser(string userAgent)
{
  PythonTuple result = pythonEngine.Execute(
    string.Format("Parse('{0}')", userAgent),
    pythonScope);
  var sb = new StringBuilder();
  sb.Append(result[0]);
  if (result[1] != null)
  {
    sb.Append("/");
    sb.Append(result[1]);
    if (result[2] != null)
      sb.Append("." + result[2]);
    if (result[3] != null)
      sb.Append("." + result[3]);
  }
  return sb.ToString();
}

Вот и всё! Парадоксально, но оно работает – и в каком-то смысле это легче чем статически компилировать питоновский код. И вообще, возможно ли это делать через IronPython? Ведь там даже проекта типа Library нам не предлагают. Коллеги, подскажите! (p.s.: подозреваю что можно попробовать компилировать это счастье в Boo, но не уверен)

Разбивка на секции (Telerik for Asp.Net AJAX)

У Телерика есть 2 набора контролов – для WebForms и для MVC. Те что для Forms – их намного больше, и они более зрелые (хотя феномен новых багов в каждой новой версии контролов присутствует – это визитная карточка фирмы). В MVC-проекте можно использовать и те и другие вперемешку.

Зачем мне WebForms-контролы? Мне понадобились сплиттеры – те вертикальные и горизонтальные разделители которые можно двигать. Понятно что можно было найти какое-нть jQuery-ориентированное решение, но раз есть лицензии, почему бы не пользоваться ими?

Работать со сплиттерами просто – тут фигурируют три контрола – RadSplitter (сам контрол), RadPane (контейнер для… вашего контента) и RadSplitBar (разделитель).

С помощью всего этого я сварганил вот такой конструкт:

Может возникнуть естественный вопрос отностительно того, что там за контролы в отсеках. Это уже контролы именно для MVC.

Начинка (Telerik MVC)

Telerik MVC распостраняется по двойной лицензии, то есть он может быть использовать в OSS-проектах. Суть такова – вы используете MVC-конструкты для определения контролов. Под MVC-конструктами я подразумеваю 3 вещи:

  • Стили, т.е. CSS-файлы и графика (тут у Телерика все супер, куча красивостей)
  • Скрипты (JS), которые подерживают AJAX-взаимодействие
  • Собственно контролы, которые создаются во вьюшке с использованием C# – это фактически дополнительные HTML-хэлперы

Тут я пожалуй покажу немного кода. Вот как создаются тэбы:

<% Html.Telerik().TabStrip()
      .Name("DetailsTabStrip")
      .Effects(fx => fx.Opacity().OpenDuration(200).CloseDuration(300))
      .Items(tabstrip =>
      {
          tabstrip.Add()
              .Text("Newsline")
              .Content(() =>
              {%>
                 ...
              <%});
          tabstrip.Add()
              .Text("Referrers")
              .Content(() =>
              {%>
                  ...
              <%});
          tabstrip.Add()
              .Text("Demographics")
              .Content(() =>
              {%>
                  ...
              <%});
          tabstrip.Add()
              .Text("Browsers")
              .Content(() =>
              {%>
                  ...
              <%});
          tabstrip.Add()
              .Text("Reporting")
              .Content(() =>
              {%>
                  ...
              <%});
      })
      .SelectedIndex(0)
      .Render(); %>

Мораль в том, что контролы MVC – это вызовы C# внутри которых можно вставлять обычный HTML. Думаю вы уже догадались как это реализовано. Но тэбы – это просто, а вот как делаются таблицы.

Таблицы через AJAX

Я люблю и часто использую таблицы из Telerik MVC – они “отнаследованы” от jqGrid, и имеют убойный набор фич (и предостаточно багов!). В нашем случае, нам нужно например справа показывать источники сбора данных, а также информацию об общем количесве посещений и среднее количесве посещений за день. Для этого мы формируем отдельную сущность:

public class SourceGridItem
{
  public string Source { get; set; }
  public int TotalVisits { get; set; }
  public double AvgVisitsPerDay { get; set; }
}

Теперь мы формируем нечто а-ля controller action именно для телериковского грида, подготавливая для него данные. На самом деле, все просто:

[GridAction]
public ActionResult SourceGridItems()
{
  using (var mongo = new Mongo("visitlogger", "localhost", "27017", ""))
  {
    var visits = mongo.GetCollection<Visit>("visits");
    var sgis = visits.Find().GroupBy(v => v.Source)
      .Select(vg => new SourceGridItem
                      {
                        Source = vg.First().Source,
                        TotalVisits = vg.Count(),
                        AvgVisitsPerDay = Math.Round(
                          vg.GroupBy(i => i.When.Date).Average(ii => ii.Count()), 2)
                      }).ToList();
    var gm = new GridModel(sgis);
    return View(gm);
  }
}

Этот метод будет вызываться таблицей через AJAX. Само определение таблицы несложное:

<%= Html.Telerik().Grid<SourceGridItem>()
  .Name("SourceGrid")
  .HtmlAttributes(new { style = "height: 100%"})
  .Columns(columns =>
  {
    columns.Bound(c => c.Source);
    columns.Bound(c => c.TotalVisits).Title("Σ");
    columns.Bound(c => c.AvgVisitsPerDay).Title("Avg/day");
  })
  .DataBinding(dataBinding => dataBinding.Ajax().Select("SourceGridItems", "Home"))
  .Scrollable()
  .Sortable()
  .Pageable(p => p.PageSize(20))
  .Filterable()
  .Groupable(c => c.Enabled(false))
  .Footer(true)
%>

Количество лямбд тут может и зашкаливает, зато приятно делать строготипизированные определения данных. Показательно, что форматирование мы делаем в методе контроллера путем Math.Round(). Возможно и стоило бы отформатировать данные на клиенте, но как-то там с форматированием не очень.

Заключение

Вы уже догадались зачем все эти лямбды в коде создания элементов? На самом деле все это счастье, если разобраться, это не Func<> а Expression<>. Супер-мега-хитрий трюк тут в том, что эти Expression<T> транслируются… в JavaScript! Вот почему в них что угодно не запишешь – ведь не все возможные выражения можно транслировать в JS.

В этом посте я показал немного “нутра” логгера. Что касается поллинга, графиков, запросов в Mongo и т.п. – это будет в следующих частях.

Код как всегда тут: http://bitbucket.org/nesteruk/visitlogger

Advertisements

Written by Dmitri

14 августа 2010 в 23:00

Опубликовано в .NET, asp.net, MongoDB, NoRM

комментария 2

Subscribe to comments with RSS.

  1. MongoDB прикольная база, но все никак не могу понять, как через эти драйвера делать выборки. Пока что делаю Find, а дальше уже при помощи LINQ, но это не вариант. Можешь подсказать какие-нибудь доки?

    Беко

    18 августа 2010 at 6:36

    • Так все зависит от драйвера и от того насколько он “допилен”. По идее, в том же NoRM можно просто писать coll.AsQueryable() и потом использовать Linq.

      Dmitri

      20 августа 2010 at 9:22


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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: