使用ELK分析Nginx日志实践

安装好ELK后,可以分析Nginx的日志查看访问量,甚至可以根据ip地址解析出GPS信息,最后生成用户访问热力图,本文是基于ELK 5.6.3版本。

1.配置Nginx日志的Logstash文件

在logstash的config目录下

vim logstash.conf

添加:

input {
        file {
                path => ["/data/log/nginx/www_access.log"]
        }
}
filter {
        grok {
                match => { "message" => "%{IPORHOST:clientip} - - \[%{HTTPDATE:time}\] \"%{WORD:verb} %{URIPATHPARAM:request} HTTP/%{NUMBER:httpversion}\" %{NUMBER:http_status_code} %{NUMBER:bytes} \"%{GREEDYDATA:referrer}\" \"%{GREEDYDATA:agent}\" \"%{GREEDYDATA:xforwardedfor}\""}
        }
        geoip {
                source => "clientip"
                        target => "geoip"
                        # fields => ["city_name", "continent_code", "country_code2", "country_code3", "country_name", "ip", "latitude", "longitude", "postal_code", "region_name", "timezone"]
                        database => "/data/src/logstash/GeoLite2-City_20171107/GeoLite2-City.mmdb"
                        add_field => [ "[geoip][coordinates]", "%{[geoip][longitude]}" ]
                        add_field => [ "[geoip][coordinates]", "%{[geoip][latitude]}"  ]
        }
        mutate {
                convert => [ "[geoip][coordinates]", "float"]
        }
}
output {
        stdout { codec => rubydebug }
        elasticsearch {
                hosts  => "127.0.0.1:9200"
                        index  => "logstash-nginx-%{+YYYY.MM.dd}"
        }
}

重点说明:

以上的配置中input下的file是Nginx的access文件。

下面filter的grok的match里的规则对应的是Nginx默认配置的日志格式,一般来说Nginx的配置文件里access日志后面加上main,下面的geoip是用来将IP地址解析成GPS信息,database是在 https://dev.maxmind.com/geoip/geoip2/geolite2/ 下载的,选择的是GeoLite2 City MaxMind DB binary gzipped,解压后将数据库mmdb格式的地址写入。

最后的output里将elasticsearch的地址写入,值得注意的是index一定要logstash打头,否则后面使用Kibana做用户访问热力图的时候会报错“No Compatible Fields: The “[nginx-access-]YYYY-MM-dd” index pattern does not contain any of the following field types: geo_point”,原因是索引格式为[nginx-access-]YYYY-MM-dd的日志文件由logstash输出到Elasticsearch;在Elasticsearch中,所有的数据都有一个类型,什么样的类型,就可以在其上做一些对应类型的特殊操作。geo信息中的location字段是经纬度,我们需要使用经纬度来定位地理位置;在Elasticsearch中,对于经纬度来说,要想使用Elasticsearch提供的地理位置查询相关的功能,就需要构造一个结构,并且将其类型属性设置为geo_point,此错误明显是由于我们的geo的location字段类型不是geo_point。

2.Kibana配置Index Patterns

到Kibana选择Management->选择Index Patterns->点击Create Index Pattern按钮->在Index pattern的输入框输入logstash-nginx-*

3.Kibana配置地图

Visualize->New->选择Maps下的Coordinate Map->选择刚刚配置的logstash-nginx-*->点击Geo Coordinates->Field选择geoip.location->点击“播放键”apply changes

最后结果:

ELK安装和实践

今年博主转战创业公司,随着系统的稳定和访问量的提升,急需一个在线的日志分析系统提供帮助,业界最知名的当属ELK了。架设这套体系的目的有三个:

1. 收集多台服务器上的日志信息,不需要上不同的服务器查看日志,方便分析处理问题。

2. 可视化界面方便搜索关键词和日志类型,处理潜在的bug。

3. 分析Nginx的访问量,是数据说话,让整个系统的运行情况了然于胸。
不多说了,直接切入正题

1. 安装Elasticsearch

下载最新的Elasticsearch tar包

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.6.3.tar.gz

解压

tar zxvf elasticsearch-5.6.3.tar.gz

进入解压后目录

cd elasticsearch-5.6.3

如果想要设置其他服务器访问,需要修改配置,将network.host修改为本机的外网或内网ip地址,如果只要用127.0.0.1就略过

vim config/elasticsearch.yml

启动Elasticsearch

bin/elasticsearch -d

2. 安装Logstash

下载最新的logstash tar包

wget https://artifacts.elastic.co/downloads/logstash/logstash-5.6.3.tar.gz

解压

tar zxvf logstash-5.6.3.tar.gz

进入解压后目录

cd logstash-5.6.3

增加配置

vim config/logstash.conf

添加:

input {
        tcp {
                mode => "server"
                host => "localhost"
                port => 4567 
                codec => multiline {
                        pattern => "^\t"                                
                        what => "previous"
                }       
        }
}
filter {
        grok {
                match => { "message" => "%{TIMESTAMP_ISO8601:time} \[%{GREEDYDATA:thread}\] \[%{GREEDYDATA:id}\] \[%{GREEDYDATA:id2}\] %{LOGLEVEL:loglevel} %{JAVACLASS:class}(\s-\s%{GREEDYDATA:info})?"}
        }
}
output {
        stdout { codec => rubydebug }
        elasticsearch {
                hosts  => "127.0.0.1:9200"
                index => "logstash-platform-%{+YYYY.MM.dd}"
        }
}

说明:以上的配置中input下的tcp是根据log4j里配置写的,在我们的JAVA工程里log4j的配置文件部分内容如下:

<Appenders>
    <!-- 日志输出到logstash -->
    <Socket name="LogStash" host="localhost" port="4567">
        <PatternLayout pattern="%d [%t] [%X{X-UUID}] [X-UID-%X{X-UID}] %-5level %logger - %msg%n" />
    </Socket>
</Appenders>
<Loggers>
    <Root level="DEBUG">
        <AppenderRef ref="Console"/>
        <AppenderRef ref="LogStash"/>
    </Root>
</Loggers>

以上XML中的host和port就对应了logstash配置中的host和port。

codec的multiline的作用是为了将exception报错输出的内容合并到一行去,即如果是以制表符\t开头的一行输入内容都连接到上一行(previous)。

filter中的grok用与匹配输入的message并做文本的截取,这里面是正则表达式,大家可以到 http://grokdebug.herokuapp.com/ 调试自己的match规则。

output里面hosts就是elasticsearch的http地址,index是logstash在elasticsearch里面创建的索引的名字,它的默认值是”logstash-%{+YYYY.MM.dd}”,我把它配置成了”logstash-platform-%{+YYYY.MM.dd}”目的是跟nginx拉过来的日志做个区分。
如果有什么问题建议大家查阅logstash官方文档,很容易理解。

最后启动logstash

nohup bin/logstash -f config/logstash.conf & > /dev/null

3. 安装Kibana

下载最新的Kibana tar包

wget https://artifacts.elastic.co/downloads/kibana/kibana-5.6.3-linux-x86_64.tar.gz

解压

tar zxvf kibana-5.6.3-linux-x86_64.tar.gz

进入解压后目录

cd kibana-5.6.3-linux-x86_64

修改配置,将elasticsearch.url改为上面安装的elasticsearch的地址

vim config/kibana.yml

启动kibana

nohup bin/kibana & > /dev/null

4. 给kibana增加登录认证

centos需要运行htpassword,先安装httpd-tools

sudo yum install -y httpd-tools

给admin这个用户设置密码

sudo htpasswd -c /data/nginx/.htpasswd admin

最后配置nginx反向代理,这里就不说了。
随着JAVA的工程启动,会忘logstash写入日志信息,这时候可以通过

curl -XGET 127.0.0.1:9200/_cat/indices

查看是否存在logstash-platform开头的索引生成。如果不正常,可以去logstash目录下的logs去查看什么原因。正常就打开Kibana,配置index pattern为logstash-platform*。

搞定后大家就可以自己去把玩Kibana发掘下它的牛逼之处了!

解决python 3.6 from http.client import HTTPSConnection ImportError: cannot import name ‘HTTPSConnection’的方法

我今天在用python3.6的时候出现了报错

from http.client import HTTPSConnection ImportError: cannot import name 'HTTPSConnection'

后来查看了python 3.6的client.py的源码和查阅了一些资料后,发现是我服务器没有安装ssl的问题。

rpm -aq|grep openssl
yum install openssl-devel -y
rpm -aq|grep openssl

然后重新编译安装python 3.6

./configure --with-ssl --enable-optimizations
make
make install

然后就ok啦!

SpringMVC视图解析器InternalResourceViewResolver问题分析

今天在搭建SpringMVC开发框架的时候,出现freemarker的视图没有找到,报404错误。我的配置代码如下:

<!--freemarker -->
<mvc:view-controller path="/" view-name="homepage/index”/>

<!-- jsp -->
<mvc:view-controller path="/login" view-name="login”/>

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
    <property name="contentType" value="text/html;charset=UTF-8"/>
    <property name="order" value="2"/>
</bean>

<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
    <property name="prefix" value="" />
    <property name="suffix" value=".ftl" />
    <property name="contentType" value="text/html;charset=UTF-8"/>
    <property name="order" value="3"/>
</bean>

我配置了两个视图解析器,所以视图解析是链式的,如果一个视图解析器没有找到对应的view-name,则开始找第二的解析器。InternalResourceViewResolver的property中的order值是2,FreeMarkerViewResolver的property的order为3,我们知道order,从小到大,越小的优先级越高,那么InternalResourceViewResolver一定是先运行的。

InternalResourceViewResolver的父类UrlBasedViewResolver中有一个方法loadView用于创建加载视图,源码如下:

/**
 * Delegates to {@code buildView} for creating a new instance of the
 * specified view class, and applies the following Spring lifecycle methods
 * (as supported by the generic Spring bean factory):
 * <ul>
 * <li>ApplicationContextAware's {@code setApplicationContext}
 * <li>InitializingBean's {@code afterPropertiesSet}
 * </ul>
 *
 * @param viewName the name of the view to retrieve
 * @return the View instance
 * @throws Exception if the view couldn't be resolved
 * @see #buildView(String)
 * @see org.springframework.context.ApplicationContextAware#setApplicationContext
 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet
 */
@Override
protected View loadView(String viewName, Locale locale) throws Exception {
    AbstractUrlBasedView view = buildView(viewName);
    View result = applyLifecycleMethods(viewName, view);
    return (view.checkResource(locale) ? result : null);
}

最后一行用到了view的checkResource方法。
我们看到InternalResourceViewResolver对应的view是InternalResourceView,它的子类只有JstlView,下面是view的类图关系。

查看InternalResourceView的父类AbstractUrlBasedView,可以找到它里面的checkResource方法,源码如下:

/**
 * Check whether the underlying resource that the configured URL points to
 * actually exists.
 *
 * @param locale the desired Locale that we're looking for
 * @return {@code true} if the resource exists (or is assumed to exist);
 * {@code false} if we know that it does not exist
 * @throws Exception if the resource exists but is invalid (e.g. could not be parsed)
 */
public boolean checkResource(Locale locale) throws Exception {
    return true;
}

可以看到这个方法直接返回true,根本就没有check。
至此我们就找到问题的原因了,InternalResourceView是不会check资源文件是否存在,当InternalResourceViewResolver先运行的时候,遇到其他的view-name如本例的freemarker的文件根本不会做check,导致最终出现404的情况。

如何解决这个问题呢?
第一种方法:把order的值修改下,把InternalResourceViewResolver的order改成最大的,即最后解析让其他的会check文件是否存在的解析器先运行。
第二种方法:自定义一个view类继承JstlView,自己写一个checkResource将父类的的checkResource override掉。代码如下:

package com.tonitech.ancestor.common;

import org.springframework.web.servlet.view.JstlView;

import java.io.File;
import java.util.Locale;

/**
* 解决多个ViewResolver时jsp获取不到时,跳转到下一个ViewResolver
*/
public class DefaultJstlView extends JstlView {

    @Override
    public boolean checkResource(Locale locale) throws Exception {
        File file = new File(this.getServletContext().getRealPath("/") + getUrl());
        return file.exists();//判断该jsp页面是否存在
    }
}

然后在InternalResourceViewResolver的bean配置里加一行:

<property name="viewClass" value="com.tonitech.ancestor.common.DefaultJstlView"/>

通过这两种方法都可以解决问题。

MySQL datetime类型使用like导致全表扫描的问题总结

最近项目里有一段mysql语句导致了数据库服务器的load过高报警,语句如下:

SELECT
    content
FROM
    communication_log
WHERE
    gmt_create LIKE '2016-12-02%';

explain了这条sql之后可以看出执行计划是全表扫描,查询了882210条记录,字段的索引没有用上,查询的时间为1.23:2be95c40-3efa-4a59-be3c-4437c1744c0a

原因是gmt_create是datetime类型,条件中写了like,会有隐式转换,gmt_create字段需要在比较前转换成String类型,字段的索引将不会被用到,查询变为全表扫描。

解决方法是把原来的SQL改成:
SELECT
    content
FROM
    communication_log
WHERE
    gmt_create BETWEEN '2016-12-02 00:00:00' AND '2016-12-02 23:59:59';

explain的结果,用上了索引只检索给定范围的行,扫描的行数也减少到了108行,查询的时间为0.02:

1b89fe5c-7e16-4a86-8ec5-c6d9c9008167