Wymored Login

como criar um analytics para sua aplicação python/tornado

09 de janeiro de 2019 por Alexandre Miguel de Andrade Souza

Podemos criar um analytics próprio para nossa aplicação python/tornado, usando a biblioteca Geoip2 e armazenando os dados em um banco postgresql.

1) Baixando as bibliotecas e arquivos para mapear ip para localização.

Instale a biblioteca geoip2 usando

pip install geoip2

Não esqueça de adicionar a lib no arquivo requirements.txt do seu projeto

geoip2

a seguir baixe o arquivo do banco de dados do geoip. Uma nova versão é lançada toda primeira terça feira do mês.

 wget https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz

e descompacte:

tar -vzxf GeoLite2-City.tar.gz

e será criada uma pasta com o arquivo GeoLite2-City.mmdb, que vamos usar. crie uma pasta private/ e mova o arquivo para lá, com renomeando o para geolite2-city.mmdb

mkdir private
cd GeoLite2-City_date/
mv GeoLite2-City.mmdb ../private/geolite2-city.mmdb

2) Criando a tabela para receber os dados:

Crie uma tabela pageview no banco de dados da aplicação:

-- DROP TABLE pageview;

CREATE TABLE pageview
(
  id serial NOT NULL,
  sitedomain character varying(512),
  url text,
  visited_at timestamp without time zone,
  title text,
  ip character varying(512),
  referrer text,
  usuario text,
  pais character varying(512),
  estado character varying(512),
  cidade character varying(512),
  latitude double precision,
  longitude double precision,
  http_user_agent text,
  http_accept_language text,
  dispositivo character varying,
  sistema_operacional character varying,
  navegador character varying,
  slug character varying,
  busca character varying,
  CONSTRAINT pageview_pkey PRIMARY KEY (id)
)
WITH (
  OIDS=FALSE
);

Você pode usar o pgadmin3 ou o psql para criar a tabela, a preferencia é sua. Para usar o psql:

sudo su postgres
psql

e no prompt do psql:

\c meudb

e cole o código sql acima.

3) O código da aplicação tornado

Nessa parte eu uso como base uma aplicação tornado, mas se usa outro framework python, basta adaptar o código para ser executado no seu framework.

Recomendo usar uma classe BaseHandler, que será utilizada para todos os Handlers (controllers no tornado, para quem está acostumado com o padrão MVC)

class BaseHandler(tornado.web.RequestHandler):
     #Nessa parte inclua as funções de acesso ao banco de dados. Veja o demo de blog em 
     #https://github.com/tornadoweb/tornado/blob/master/demos/blog/blog.py

     #crie ou adapte a função prepare:
     async def prepare(self):
       dispositivo = 'PC'
        user_agent = self.request.headers["User-Agent"]
        so = ['Windows','Android','Linux','iPhone OS']
        nav = ['Firefox','Chrome','Trident','Safari']
        mobile = ['Android','iPhone']
        sistema_operacional = ''
        navegador = ''

        for d in mobile:
            if d in user_agent:
                dispositivo = 'celular'
                break
        for d in so:
            if d in user_agent:
                sistema_operacional = d
                break  
        for d in nav:
            if d in user_agent:
                navegador = d
                break

        import geoip2.database

        f = os.path.join(os.path.dirname(__file__),'private','geolite2-city.mmdb')
        reader = geoip2.database.Reader(f)

        pais, estado, cidade, latitude, longitude = None, None, None, None, None

        try:
            r = reader.city(self.request.remote_ip)
            pais = r.contry.names['pt-BR']
            estado = r.subdivisions.most_especific.name
            cidade = r.city.name
            latitude = r.location.latitude
            longitude = r.location.longitude
        except:
            pass
        busca = self.get_arguments('search') 
        if len(busca)==0:
            busca = None
        uri_split = self.request.uri.split('/')
        if len(uri_split) > 1:
            secao = uri_split[1]
        else:
            secao = None
        if len(uri_split)>2:
            slug = uri_split[2]
        else:
            slug = None

        await self.execute("""INSERT INTO pageview ( sitedomain,url, visited_at, ip, referrer,
        usuario, pais, estado, cidade, latitude, longitude, http_user_agent,
        http_accept_language, dispositivo, sistema_operacional,  navegador, secao,
        slug,  busca) 
        values
        ( %s, %s, CURRENT_TIMESTAMP, %s, %s, %s, %s, %s, %s,  %s, %s, %s,
        %s, %s,%s,%s, %s, %s,%s )""",
        host,self.request.uri,self.request.remote_ip, user, self.request.headers.get('Referer'),
        pais, estado, cidade, latitude, longitude, user_agent, self.request.headers.get('Accept-Language'),
        dispositivo, sistema_operacional,
        navegador, secao, slug, busca)

Pronto, a parte de incluir os dados já está pronta.

Agora, derive todos os seus controllers a partir da classe acima:

  class HomeHandler(BaseHandler):
      #resto do código

4) Dashboard/Analytics

Para criar o dashboard, usei um template do w3.css, disponível em Demo Analytics

O primeiro passo é adicionar no route do app, em

class Application(tornado.web.Application):
    def __init__(self, db):
        self.db = db
        handlers = [
            (r"/", HomeHandler),
            #outros handlers aqui....
            (r"/analytics", AnalyticsHandler), #insira esta linha
            (r"/.*", NotFoundHandler),
        ]

O próximo passo é criar o Handler (controller no padrão MVC) :

class AnalyticsHandler(BaseHandler):
    async def get(self):
        visualizacoes_dia = await self.queryone("""select count(id) as n
                    from pageview 
                    where date(visited_at) = current_date""")

        visualizacoes_semana = await self.queryone("""select count(id) as n
                    from pageview 
                    where date_trunc('week', visited_at) = date_trunc('week', current_date)""")
        visualizacoes_mes = await self.queryone("""select count(id) as n
                    from pageview 
                    where date_trunc('month', visited_at) = date_trunc('month', current_date)""")

        usuarios_dia = await self.queryone("""with u as (select distinct usuario
from pageview
where date(visited_at) = current_date)

select count(usuario) as n from u""")

        usuarios_semana = await self.queryone("""with u as (select distinct usuario
from pageview
where date_trunc('week', visited_at::date) = date_trunc('week',current_date))

select count(usuario)  as n from u""")

        usuarios_mes = await self.queryone("""with u as (select distinct usuario
from pageview
where date_trunc('week', visited_at::date) = date_trunc('week',current_date))

select count(usuario) as n from u""")

        artigos_dia = await self.queryone("""select count(id) as n
                from entries
                where date(published) = current_date""")
        artigos_semana = await self.queryone("""select count(id) as n
                from entries
                where date_trunc('week',published) = date_trunc('week',current_date)""")                  
        artigos_mes = await self.queryone("""select count(id) as n
                from entries
                where date_trunc('month',published) = date_trunc('month',current_date)""")    
        paginas_mes = await self.query("""select split_part(url,'?',1) as url, count(id) as
                from pageview
                where date_trunc('month',visited_at) = date_trunc('month',current_date)
                group by 1
                order by 2 desc""")

        self.render("analytics.html", visualizacoes_dia=visualizacoes_dia.n,visualizacoes_semana=visualizacoes_semana.n,
        visualizacoes_mes=visualizacoes_mes.n,
        artigos_dia=artigos_dia.n,artigos_semana=artigos_semana.n,artigos_mes=artigos_mes.n,
        usuarios_dia=usuarios_dia.n, usuarios_semana=usuarios_semana.n,usuarios_mes=usuarios_mes.n,
        paginas_mes=paginas_mes)

O último passo é criar a view dentro da pasta templates/. crie um arquivo analytics.html com o conteúdo:

 {% extends "base.html" %}

{% block head %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ static_url("w3.css") }}" type="text/css"> 
<link rel="stylesheet" href="{{ static_url("admin.css") }}" type="text/css"> 
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<style>
  html,body,h1,h2,h3,h4,h5 {font-family: "Raleway", sans-serif}
  </style>
{% end %}

{% block body %}

<!-- Sidebar/menu -->
<nav class="w3-sidebar w3-collapse w3-white w3-animate-left" style="z-index:3;width:300px;" id="mySidebar"><br>
  <div class="w3-container w3-row">
    <div class="w3-col s4">
      <img src="{{ static_url("images/avatar2.png" )}} class="w3-circle w3-margin-right" style="width:46px">
    </div>
    <div class="w3-col s8 w3-bar">
      <span>Bem vindo, <strong>Admin</strong></span><br>

    </div>
  </div>
  <hr>
  <div class="w3-container">
    <h5>Painel de Controle</h5>
  </div>
  <div class="w3-bar-block">
    <a href="#painel" class="w3-bar-item w3-button w3-padding-16 w3-hide-large w3-dark-grey w3-hover-black" onclick="w3_close()" title="close menu"><i class="fa fa-remove fa-fw"></i>&nbsp; Close Menu</a>
    <a href="#painel" class="w3-bar-item w3-button w3-padding w3-blue"><i class="fa fa-users fa-fw"></i>&nbsp; Visão Geral</a>
    <a href="#metas" class="w3-bar-item w3-button w3-padding"><i class="fa fa-eye fa-fw"></i>&nbsp; Metas</a>
    <a href="#paginas" class="w3-bar-item w3-button w3-padding"><i class="fa fa-users fa-fw"></i>&nbsp; Páginas</a>
</div>
</nav>


<!-- Overlay effect when opening sidebar on small screens -->
<div class="w3-overlay w3-hide-large w3-animate-opacity" onclick="w3_close()" style="cursor:pointer" title="close side menu" id="myOverlay"></div>

<!-- !PAGE CONTENT! -->
<div class="w3-main" style="margin-left:300px;margin-top:43px;">

  <!-- Header -->
  <header class="w3-container" style="padding-top:22px">
    <h5 id="painel"><b><i class="fa fa-dashboard"></i> Painel de Controle</b></h5>
    <h6>Hoje</h6>
  </header>
  <div class="w3-row-padding w3-margin-bottom">
  <div class="w3-quarter">
    <div class="w3-container w3-blue w3-padding-16">
      <div class="w3-left"><i class="fa fa-eye w3-xxxlarge"></i></div>
      <div class="w3-right">
        <h3>{{ visualizacoes_dia }}</h3>
      </div>
      <div class="w3-clear"></div>
      <h4>Visualizações</h4>
    </div>
  </div>

  <div class="w3-quarter">
    <div class="w3-container w3-orange w3-text-white w3-padding-16">
      <div class="w3-left"><i class="fa fa-users w3-xxxlarge"></i></div>
      <div class="w3-right">
        <h3>{{usuarios_dia}}</h3>
      </div>
      <div class="w3-clear"></div>
      <h4>Usuarios</h4>
    </div>
  </div>

    <div class="w3-quarter">
      <div class="w3-container w3-red w3-padding-16">
        <div class="w3-left"><i class="fa fa-files-o w3-xxxlarge"></i></div>
        <div class="w3-right">
          <h3>{{artigos_dia}}</h3>
        </div>
        <div class="w3-clear"></div>
        <h4>Artigos</h4>
      </div>
    </div>
  </div>

  <header class="w3-container" style="padding-top:22px">
  <h6>Esta semana</h6>
</header>
<div class="w3-row-padding w3-margin-bottom">
<div class="w3-quarter">
  <div class="w3-container w3-blue w3-padding-16">
    <div class="w3-left"><i class="fa fa-eye w3-xxxlarge"></i></div>
    <div class="w3-right">
      <h3>{{ visualizacoes_semana }}</h3>
    </div>
    <div class="w3-clear"></div>
    <h4>Visualizações</h4>
  </div>
</div>

<div class="w3-quarter">
  <div class="w3-container w3-orange w3-text-white w3-padding-16">
    <div class="w3-left"><i class="fa fa-users w3-xxxlarge"></i></div>
    <div class="w3-right">
      <h3>{{usuarios_semana}}</h3>
    </div>
    <div class="w3-clear"></div>
    <h4>Usuarios</h4>
  </div>
</div>

  <div class="w3-quarter">
    <div class="w3-container w3-red w3-padding-16">
      <div class="w3-left"><i class="fa fa-files-o w3-xxxlarge"></i></div>
      <div class="w3-right">
        <h3>{{artigos_semana}}</h3>
      </div>
      <div class="w3-clear"></div>
      <h4>Artigos</h4>
    </div>
  </div>
</div>

<!-- mes-->
<header class="w3-container" style="padding-top:22px">
  <h6>Este mês</h6>
</header>
<div class="w3-row-padding w3-margin-bottom">
<div class="w3-quarter">
  <div class="w3-container w3-blue w3-padding-16">
    <div class="w3-left"><i class="fa fa-eye w3-xxxlarge"></i></div>
    <div class="w3-right">
      <h3>{{ visualizacoes_mes }}</h3>
    </div>
    <div class="w3-clear"></div>
    <h4>Visualizações</h4>
  </div>
</div>

<div class="w3-quarter">
  <div class="w3-container w3-orange w3-text-white w3-padding-16">
    <div class="w3-left"><i class="fa fa-users w3-xxxlarge"></i></div>
    <div class="w3-right">
      <h3>{{usuarios_mes}}</h3>
    </div>
    <div class="w3-clear"></div>
    <h4>Usuarios</h4>
  </div>
</div>

  <div class="w3-quarter">
    <div class="w3-container w3-red w3-padding-16">
      <div class="w3-left"><i class="fa fa-files-o w3-xxxlarge"></i></div>
      <div class="w3-right">
        <h3>{{artigos_mes}}</h3>
      </div>
      <div class="w3-clear"></div>
      <h4>Artigos</h4>
    </div>
  </div>
</div>



  <div class="w3-container">
    <h5 id="metas">Metas</h5>
    <p>Visualizações/Mês</p>
    <div class="w3-grey">
      <div class="w3-container w3-center w3-padding w3-green" style="width:{{visualizacoes_mes/5000*100}}%">{{visualizacoes_mes}}/5000</div>

    </div>

    <p>Usuários/Mês</p>
    <div class="w3-grey">
      <div class="w3-container w3-center w3-padding w3-orange" style="width:{{usuarios_mes/5000*100}}%">{{usuarios_mes}}/2000</div>
    </div>

    <p>Artigos/Mês</p>
    <div class="w3-grey">
      <div class="w3-container w3-center w3-padding w3-red" style="width:{{artigos_mes/50*100}}%">{{usuarios_mes}}/50</div>
    </div>
  </div>
  <hr>

  <div class="w3-container">
    <h5 id="Páginas">Páginas mais visualizadas</h5>
    <table class="w3-table w3-striped w3-bordered w3-border w3-hoverable w3-white">
      <tbody>
        {% for page in paginas_mes %}
        <tr>
        <td>{{page.url}}</td>
        <td>{{page.n}}</td>
      </tr>
      {% end %}

    </tbody></table><br>
</div>
  <hr>

  <!-- End page content -->
</div>

<script>
// Get the Sidebar
var mySidebar = document.getElementById("mySidebar");

// Get the DIV with overlay effect
var overlayBg = document.getElementById("myOverlay");

// Toggle between showing and hiding the sidebar, and add overlay effect
function w3_open() {
  if (mySidebar.style.display === 'block') {
    mySidebar.style.display = 'none';
    overlayBg.style.display = "none";
  } else {
    mySidebar.style.display = 'block';
    overlayBg.style.display = "block";
  }
}

// Close the sidebar with the close button
function w3_close() {
  mySidebar.style.display = "none";
  overlayBg.style.display = "none";
}
</script>

{% end %}

Inté!