Engenharia

GeoIP no Elastic Stack

Introdução

Uma das coisas legais do Elastic Stack é que você consegue encontrar verdadeiros tesouros escondidos em simples arquivos de log. Isso vai desde descobrir o consumo de banda dos seus usuários internos até ajudar no planejamento da expansão do quadro de pessoal e da capacidade de Internet para todas as suas filiais. Também dá para descobrir quais navegadores acessam seu site para poder escolher entre a otimização para dispositivos móveis ou desktop. Ou ver exatamente de onde seus usuários finais estão se conectando para que as equipes de marketing e vendas possam aperfeiçoar a estratégia de engajamento. Tudo isso é possível seguindo alguns passos simples.

Percebemos que é comum perguntarem como transformar endereços IP ou nomes de hosts ⎻ ou seja, a representação de um host ou sistema que as pessoas usam para acessar outros sistemas na Internet ⎻ em pontos de geolocalização (latitude e longitude) para responder aos tipos de perguntas que acabamos de fazer. Em outras palavras, traduções de geoip.

Este post esmiúça o básico para você começar a usar o geoip no Elastic Stack e traz algumas dicas para resolver problemas comuns.

Aperfeiçoe!

Vamos usar esta linha de log fictícia do Apache como dados de exemplo:

104.194.203.69 - - [01/Apr/2017:16:21:15 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36"

Agora, vamos usar a Ingest API e o Logstash para demonstrar como é possível aplicar mais de um método para processar seus dados. Os métodos seguirão passos bem parecidos para converter um endereço IP ou nome de host em um conjunto de coordenadas geográficas, o que significa que é possível intercambiar facilmente entre eles.

Falando rapidamente sobre o que vai acontecer, a Ingest API e o Logstack usam as informações de IP/nome do host de um evento para fazer uma pesquisa em uma cópia armazenado internamente do banco de dados gratuito GeoLite2 da MaxMind e criam diversos campos extras no evento com as coordenadas geográficas e outros campos, como cidade, estado e país. A partir daí, podemos plotar esses pontos em um mapa no Kibana.

Vamos colocar a mão na massa para ver como todo esse processo funciona. :)

Ingest API

A Ingest API foi adicionada ao Elasticsearch 5.0 e é um jeito ótimo de aproveitar os recursos de cluster existentes para processar documentos, usando coisas como grok para encontrar e extrair padrões. Até agora, os casos de uso incluem o envio de logs diretamente para o Elasticsearch a partir do Filebeat ou o processamento de documentos Arxiv.org com o plugin ingest-attachment (o substituto do mapper-attachment).

E se essas possibilidades não atenderem às suas necessidades, você poderá desenvolver seu próprio plugin ingest usando este modelo, por exemplo, para proporcionar uma funcionalidade de processamento de linguagem natural ao Elasticsearch de forma nativa.

No nosso caso de uso, precisamos instalar o plugin geo-ip, pois ele não vem incluído por padrão. Então, vamos executar a seguinte linha em todos os nós do cluster que temos:

sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install ingest-geoip

Em seguida, reinicie o(s) nó(s) para ativar o plugin.

Agora, uma das vantagens da Ingest API é que ela permite simular um pipeline: basta executar um teste do que as etapas de processamento do evento geram, mas sem indexar o evento ao Elasticsearch. Assim, você consegue avaliar seu processador com antecedência e, se for o caso, fazer as alterações necessárias.

Para fazer essa simulação, nós simplesmente definimos o pipeline e entregamos nosso documento de exemplo da seguinte maneira (nota: é preciso usar barras invertidas como caractere de escape antes das aspas do evento):

POST _ingest/pipeline/_simulate
{
  "pipeline" : {
"processors" : [
    {
      "grok": {
        "field": "message",
        "patterns": ["%{COMBINEDAPACHELOG}"]
      }
    },
    {
      "geoip": {
        "field": "clientip"
      }
    }
  ]
  },
  "docs": [
    {
      "_source": {
        "message": "104.194.203.69 - - [01/Apr/2017:16:21:15 +0000] \"GET /favicon.ico HTTP/1.1\" 200 3638 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36\""
      }
    }
  ]
}

Quem já conhece o Logstash está vendo que estamos usando um padrão grok predefinido chamado %{COMBINEDAPACHELOG}. Dentro dele, há um campo %{IPORHOST:clientip} que usaremos depois no processador geoip para obter as informações que queremos.

O resultado obtido desta chamada é:

{
  "docs": [
    {
      "doc": {
        "_type": "_type",
        "_id": "_id",
        "_index": "_index",
        "_source": {
          "request": "/favicon.ico",
          "agent": """"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36"""",
          "geoip": {
            "continent_name": "Oceania",
            "city_name": "Alexandria",
            "country_iso_code": "AU",
            "region_name": "New South Wales",
            "location": {
              "lon": 151.2,
              "lat": -33.9167
            }
          },
          "auth": "-",
          "ident": "-",
          "verb": "GET",
          "message": """104.194.203.69 - - [01/Apr/2017:16:21:15 +0000] "GET /favicon.ico HTTP/1.1" 200 3638 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36"""",
          "referrer": """"-"""",
          "response": "200",
          "bytes": "3638",
          "clientip": "104.194.203.69",
          "httpversion": "1.1",
          "timestamp": "01/Apr/2017:16:21:15 +0000"
        },
        "_ingest": {
          "timestamp": "2017-04-11T04:19:00.518Z"
        }
      }
    }
  ]
}

Em seguida, podemos salvar este pipeline no Elasticsearch, definir um modelo para garantir que os valores de geoip sejam devidamente tratados e usar o Filebeat para enviar os dados direto ao nosso cluster para indexação e armazenamento, com as informações de geoip adicionadas de forma automática.

Logstash

Como na Ingest API, o filtro geoip é o nosso ponto de partida no Logstash. Ele faz parte do pacote padrão do Logstash, por isso, é só irmos direto para a nossa configuração.

Uma configuração básica para processar nosso evento ficará assim:

input { stdin {} }
filter {
  grok { match => { "message" => "%{COMBINEDAPACHELOG}" } }
  geoip { source => "clientip" }
}
output { stdout { codec => "rubydebug" } }

Novamente, estamos usando um padrão grok predefinido e, como é de se esperar, os padrões grok da Ingest API são os mesmos do filtro grok no Logstash. Então, podemos reutilizar essa funcionalidade tranquilamente.

Depois de processar os dados de exemplo com a nossa configuração, temos:

{
        "request" => "/favicon.ico",
          "agent" => "\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36\"",
          "geoip" => {
              "timezone" => "Australia/Sydney",
                    "ip" => "104.194.203.69",
              "latitude" => -33.9167,
        "continent_code" => "OC",
             "city_name" => "Alexandria",
         "country_code2" => "AU",
          "country_name" => "Australia",
         "country_code3" => "AU",
           "region_name" => "New South Wales",
              "location" => [
            [0] 151.2,
            [1] -33.9167
        ],
           "postal_code" => "1435",
             "longitude" => 151.2,
           "region_code" => "NSW"
    },
           "auth" => "-",
          "ident" => "-",
           "verb" => "GET",
        "message" => "104.194.203.69 - - [01/Apr/2017:16:21:15 +0000] \"GET /favicon.ico HTTP/1.1\" 200 3638 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36\"",
       "referrer" => "\"-\"",
     "@timestamp" => 2017-04-14T00:40:18.814Z,
       "response" => "200",
          "bytes" => "3638",
       "clientip" => "104.194.203.69",
       "@version" => "1",
           "host" => "bender.local",
    "httpversion" => "1.1",
      "timestamp" => "01/Apr/2017:16:21:15 +0000"
}

Observe que a localização é, de fato, um conjunto de coordenadas que mantêm as informações de latitude e longitude em campos separados caso seja necessário usá-las. Pronto!

Considerando que queremos armazenar os dados no Elasticsearch para podermos analisá-los com o Kibana, é preciso alterar a saída para usar o plugin de saída do elasticsearch.

Mapear para mapas

O Logstash e o Filebeat são capazes de autogerenciar o modelo necessário para garantir que um campo geoip enviado ao Elasticsearch seja tratado corretamente como um dado do tipo geo_point.

Supondo que estamos usando o padrão de nome de índice pré-definido do Logstash ou do Filebeat, enviar o nosso evento de exemplo do Logstash para o Elasticsearch significa que também estamos aplicando o modelo padrão que faz o envio com o Logstash ou com o Filebeat. As partes específicas que nos interessam são:

"geoip"  : {
  "dynamic": true,
  "properties" : {
    "ip": { "type": "ip" },
    "location" : { "type" : "geo_point" },
    "latitude" : { "type" : "half_float" },
    "longitude" : { "type" : "half_float" }
  }
}

Tudo isso significa que todos os campos chamados geoip.location ⎻ ou seja, a forma como nos referimos a esses campos aninhados ⎻ que forem enviados ao Elasticsearch serão automaticamente mapeados como um geo_point.

Se você estiver usando diretamente a Ingest API, será necessário ter certeza de que existe um modelo aplicado. A opção mais fácil aqui é copiar os modelos do Logstash ou do Filebeat acima (nos links) e alterá-los conforme o caso.

Os leitores mais observadores devem ter percebido que as saídas de dados geoip da Ingest API e do Logstash apresentaram formatos diferentes. O Elasticsearch aceita diversos formatos geoip, assim como o padrão GeoJSON também é permitido. Consulte os diferentes formatos compatíveis na documentação.

Vamos marcar os pontos

Agora que já processamos os dados dentro do Elasticsearch, chegou a hora de montarmos alguns gráficos. Após adicionarmos um padrão de índice ao Kibana, podemos confirmar se o campo está definido como um ponto geográfico. Um campo mapeado corretamente ficará assim de acordo com as configurações do padrão de índice:

index-pattern-setting-geoip-location

E assim no Discover:

discover-geoip-location

Agora que está tudo confirmado e preenchido, acesse a seção Visualisation e crie um novo mapa de blocos, usando o padrão de índice que você acabou de definir. Depois de escolher o padrão, e supondo que os seus pontos geográficos estejam corretamente mapeados, o Kibana vai preencher automaticamente as configurações de visualização como qual campo deve ser agregado, exibindo o mapa quase instantaneamente. Ficará mais ou menos assim:

screenshot-kibana-dashboard

Em nosso exemplo, temos apenas um ponto de dados (oi, Austrália!), mas se houver mais pontos, será possível aumentar o zoom e ter uma vista panorâmica. O Kibana vai redistribuir os dados agregados conforme o necessário, e você pode até criar um quadro de delimitação geográfica ou um filtro para obter dados específicos.

Veja um exemplo de mapa mais completo. Esses dados são 100% reais e foram extraídos dos visitantes do nosso blog, ou seja, você está bem ali!

screenshot-populated-map-example

E se eu não conseguir criar um mapa?

Nem sempre as coisas saem conforme o planejado. Você já indexou seus dados ao Elasticsearch e agora quer usá-los no Kibana, mas, ao tentar criar um novo mapa de blocos, não há campos disponíveis. Que tristeza!

Esse é um problema que os usuários enfrentam quando trabalham com geoip, e a causa dele pode ser alguns antipadrões comuns.

Mover as coisas para campos personalizados

Às vezes, vemos configurações como esta:

geoip {
  source => "src_ip"
  target => "geoip"
  add_field => [ "[geoip][coordinates]", "%{[geoip][longitude]}" ]
  add_field => [ "[geoip][coordinates]", "%{[geoip][latitude]}"  ]
}
mutate {
  convert => [ "[geoip][coordinates]", "float"]
}

O filtro geoip já cria um campo de localização, como vimos nos exemplos acima. Portanto, o uso de add_field apenas duplica os dados, perdendo a correspondência com o modelo padrão do Elasticsearch. Isso quer dizer que os dados provavelmente serão mapeados dinamicamente conforme uma matriz numérica, impedindo que você use um mapa no Kibana sem fazer modificações.

Outra variação é alterar o valor padrão de target (geoip). Se você fizer isso, será necessário alterar o mapeamento/o modelo de índice para resolver o problema. Veremos mais sobre isso depois.

Nomes de índices customizados

Ao usar um nome/padrão de índice customizado na saída do Elasticsearch, ou alterar o campo de destino padrão para os dados geoip, será necessário copiar o modelo padrão do Logstash/Filebeat que mencionamos acima e garantir que eles correspondam a essas alterações. Para fazer isso, você pode copiar o arquivo que o Logstash/Filebeat usa e fazer a menção à alteração um, ou pegar o modelo das APIS do Elasticsearch e fazer essa modificação.

Uma dica rápida caso você opte por esse método: pesquise e atualize o valor "template": "logstash-*" no modelo ou no arquivo.

Por fim, pode ser mais fácil usar um nome de índice como logstash-customvalue-YYYY.MM.DD (onde customvalue é um valor da sua escolha), em vez de alterar todo o padrão. Assim, o modelo padrão ainda pode ser usado.

_geoip_lookup_failure

Caso esta mensagem apareça, o Logstash não conseguiu encontrar o campo que você indicou que contém um endereço IP ou um nome de host para o mapa. Confira se seu padrão grok está extraindo o campo corretamente e se seu filtro geoip está fazendo a referência correta.

O uso de uma seção stdout { codec => rubydebug } na saída do Logstash também pode ajudar você a identificar o que está (ou não está) acontecendo.

Geolocalização imprecisa

É possível que o endereço IP coincida com uma localização incorreta. Leve em consideração que o banco de dados gratuito da MaxMind utilizado é “comparável, mas menos preciso do que os bancos de dados GeoIP2 da MaxMind”, e que “a geolocalização por IP é essencialmente imprecisa. Os locais geralmente são próximos ao centro populacional”. Consulte o site da MaxMind para mais detalhes.

Existem bancos de dados disponíveis no mercado que podem ser mais precisos. Para usá-los, será preciso definir o caminho do banco de dados nas configurações do Logstash ou no database_file da Ingest API para apontar para o arquivo do banco de dados adquirido.

Nenhum resultado encontrado

Confira se seu seletor de período (no canto superior direito da janela) mostra o período correto para os seus dados. Em caso positivo, acesse “Discover” e confira se os eventos do período têm mesmo um campo geoip.

Se ainda assim não aparecer nenhum resultado, confira novamente os tópicos cobertos no início deste post, especialmente os mapeamentos.

Eu vejo os pontos de dados, mas não vejo mapas

O Kibana usa o serviço Elastic Tile Map para apresentar os blocos que formam o mapa.  No entanto, algumas organizações podem bloquear o acesso a esse serviço com firewalls ou proxies, impedindo a exibição da sua região geográfica favorita no navegador. Sabemos também que plugins de navegador, como o Privacy Badger, impedem o carregamento dos blocos. Então, verifique se esse não é o caso (talvez, experimente acessar por uma guia no modo anônimo).

Se não for possível evitar as políticas de segurança, considere configurar seu próprio serviço de mapa de blocos ou aponte o Kibana para outro serviço compatível com WMS, como menciona a documentação.

Conclusão

Como você pode ver, a extração do geoip tem muito valor para diversos casos de uso. E, seguindo apenas alguns passos, nós mostramos dois métodos simples para obter essas informações de um evento para exibi-las graficamente com o Kibana.

Não abordamos detalhadamente a possibilidade de usar mapas customizados, mas se quiser se inspirar ou obter orientações, confira um desses dois posts: Kibana and a Custom Tile Server for NHL Data ou Earthquake data with the Elastic Stack.

Por fim, não pense demais na configuração. Siga os padrões nos nomes de campo e de índice sempre que puder e, na dúvida, consulte a documentação.

Se os problemas persistirem, ou em caso de dúvidas, recorra aos fóruns da nossa comunidade e conte com a ajuda dos nossos incríveis Mantenedores da Comunidade Logstash e de toda a equipe Elastic.