自定义flume拦截器-实现了多种功能

1. 自定义拦截器实现说明

    1. 实现interceptor接口,并实现其方法,接口完全限定名为:org.apache.flume.interceptor.Interceptor;

    2. 自定义拦截器内部添加静态内部类,实现Builder接口,并实现其方法,接口完全限定名为:Interceptor.Builder

    以下是最简单的代码示例(每个方法的作用都有注释说明):

    package cn.com.bonc.interceptor;

import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * 自定义拦截器,实现Interceptor接口,并且实现其抽象方法
 */
public class CustomInterceptor implements Interceptor {

    //打印日志,便于测试方法的执行顺序
    private static final Logger logger = LoggerFactory.getLogger(CustomLogger.class);
    //自定义拦截器参数,用来接收自定义拦截器flume配置参数
    private static String param = "";


    /**
     * 拦截器构造方法,在自定义拦截器静态内部类的build方法中调用,用来创建自定义拦截器对象。
     */
    public CustomInterceptor() {
        logger.info("----------自定义拦截器构造方法执行");
    }


    /**
     * 该方法用来初始化拦截器,在拦截器的构造方法执行之后执行,也就是创建完拦截器对象之后执行
     */
    @Override
    public void initialize() {
        logger.info("----------自定义拦截器的initialize方法执行");
    }

    /**
     * 用来处理每一个event对象,该方法不会被系统自动调用,一般在 List<Event> intercept(List<Event> events) 方法内部调用。
     *
     * @param event
     * @return
     */
    @Override
    public Event intercept(Event event) {
        logger.info("----------intercept(Event event)方法执行,处理单个event");
        logger.info("----------接收到的自定义拦截器参数值param值为:" + param);
        /*
        这里编写event的处理代码
         */
        
return event;
    }

    /**
     * 用来处理一批event对象集合,集合大小与flume启动配置有关,和transactionCapacity大小保持一致。一般直接调用 Event intercept(Event event) 处理每一个event数据。
     *
     * @param events
     * @return
     */
    @Override
    public List<Event> intercept(List<Event> events) {

logger.info("----------intercept(List<Event> events)方法执行");

        /*
        这里编写对于event对象集合的处理代码,一般都是遍历event的对象集合,对于每一个event对象,调用 Event intercept(Event event) 方法,然后根据返回值是否为null
        来将其添加到新的集合中。
         */
        
List<Event> results = new ArrayList<>();
        Event event;
        for (Event e : events) {
            event = intercept(e);
            if (event != null) {
                results.add(event);
            }
        }
        return results;
    }

    /**
     * 该方法主要用来销毁拦截器对象值执行,一般是一些释放资源的处理
     */
    @Override
    public void close() {
        logger.info("----------自定义拦截器close方法执行");
    }

    /**
     * 通过该静态内部类来创建自定义对象供flume使用,实现Interceptor.Builder接口,并实现其抽象方法
     */
    public static class Builder implements Interceptor.Builder {

        /**
         * 该方法主要用来返回创建的自定义类拦截器对象
         *
         * @return
         */
        @Override
        public Interceptor build() {
            logger.info("----------build方法执行");
            return new CustomInterceptor();
        }

        /**
         * 用来接收flume配置自定义拦截器参数
         *
         * @param context 通过该对象可以获取flume配置自定义拦截器的参数
         */
        @Override
        public void configure(Context context) {
            logger.info("----------configure方法执行");
            /*
            通过调用context对象的getString方法来获取flume配置自定义拦截器的参数,方法参数要和自定义拦截器配置中的参数保持一致+
             */
            
param = context.getString("param");
        }
    }
}

    3. 将自定义拦截器打包,上传,我使用的是CDH,自定义拦截器jar包位置为:/opt/cloudera/parcels/CDH-5.12.0-1.cdh5.12.0.p0.29/lib/flume-ng/lib,如果使用的原生的flume,大家自行查询自定义拦截器jar包存放位置即可。

    4. 编写flume配置文件,下面是示例代码:

#agent
customInterceptor.sources=r1
customInterceptor.channels=c1
customInterceptor.sinks=s1

#source
customInterceptor.sources.r1.type=spooldir
customInterceptor.sources.r1.spoolDir=/home/jumpserver/flume/data1
customInterceptor.sources.r1.consumeOrder=youngest
customInterceptor.sources.r1.recursiveDirectorySearch=false
customInterceptor.sources.r1.deletePolicy=immediate
customInterceptor.sources.r1.pollDelay=500
#source1-interceptor
customInterceptor.sources.r1.interceptors=i1
customInterceptor.sources.r1.interceptors.i1.type=cn.com.bonc.interceptor.CustomInterceptor$Builder
customInterceptor.sources.r1.interceptors.i1.param=parameter


#channe1
customInterceptor.channels.c1.type=memory
customInterceptor.channels.c1.capacity=1000
customInterceptor.channels.c1.transactionCapacity=100

#sink
customInterceptor.sinks.s1.type=logger

#package
customInterceptor.sources.r1.channels=c1
customInterceptor.sinks.s1.channel=c1

  注意配置自定义拦截器部分,红色字体部分。i1.param这儿的param要和自定义拦截器代码中的context .getString(“param”)参数保持一致!

    4. 启动flume

        命令:flume-ng agent -c ./ -f customInterceptor.conf -n customInterceptor | grep INFO

    为了查看方便,我只过滤出了INFO级别的信息。

    结果:

         

            从运行结果中可以看出每个方法的执行顺序。

  现在我往/home/jumpserver/flume/data1目录中拷贝文件,然后flume就会处理该文件:

            data.txt内容为:

                this is a test string.

                cp data.txt flume/data1/

        结果:    从运行结果中可以看出每个方法的执行顺序。

 现在重新运行flume,这次打印出所有的日志,以此来查看正常结束(Ctrl+C或者是kill -2flume时的运行结果:    

  从运行结果中可以看出flume正常结束时,close方法被调用了。

2. 功能说明

  这两天公司给了我个需求:监视服务器(linux系统)的一个本地目录,不断有人往里面上传新的文件,有五分钟一个文件的,两分钟一个文件的,一分钟一个文件的,30秒一个文件的,还有不定时一个文件的,而且会有同名文件上传(重点)!!!要求我对于不同的文件,按照文件名来区分,将杂乱的文件内容处理完之后,将其上传至不同的HDFS目录下,以便于Hive表管理(重点)!!!。实现的具体多个功能如下:

    1. spooldir处理同名文件。

    2. 自定义过滤器处理杂乱的文件内容,去除多余的行,并修改每行内容,同理,也可增加行。

    3. 过滤器接收自定义参数来处理不用的文件名,HDFS sink通过匹配文件名(最好的方法为正则匹配)来将不同文件保存至不同的HDFS路径下(按天动态分区)。

3. 功能实现思路(与上面问题一一对应)

    1. spooldir默认是将处理完的新文件重命名,原文件名添加后缀名.COMPLETE,但是遇到同名文件之后,除了完同名文件,在重命名时,发现已经有相同文件名的文件存在,此时flume会报错,然后停止监控线程(此线程默认500毫秒扫描一次目录,检查是否有新文件),目录中再有新的文件,也不会被处理。

        处理方法为:设置spooldir文件删除策略为:data.sources.r1.deletePolicy=immediate

    2. spooldir默认按行读取文件内容,因此每个event body中的内容都是一行文件内容。要实现减少或者是增加event的效果(也即是删除或者是增加文件内容行数),就要在public Event intercept(Event event)动手了。删除event的话,只需要在该方法中返回null即可;如果想要增加event,可以自定义一个处理方法,接受Event event参数,返回List<Event>,也就是:public List<Event> intercept2(Event event)

    3. 在自定义拦截器中添加静态变量,然后在静态内部类中的configure方法中通过Context context参数获取参数值,然后赋值给自定义静态变量。

4. 功能代码演示

    1. 配置代码:

customInterceptor.sources.r1.type=spooldir
customInterceptor.sources.r1.spoolDir=/home/jumpserver/flume/data1
customInterceptor.sources.r1.consumeOrder=youngest
customInterceptor.sources.r1.recursiveDirectorySearch=false
customInterceptor.sources.r1.deletePolicy=immediate
customInterceptor.sources.r1.pollDelay=500

        注意红色字体。

    2. 去除文件头,并且将文件头信息加入每行数据的代码:

        被处理文件原始内容:
            1111111;唐口煤矿5306工作面;KJ615|

            2018-03-20 07:14:29|

            13;4.66~

            6;4.23~||

        被处理之后的内容;

             1111111,唐口煤矿5306工作面,KJ615,2018-03-20 07:14:29,13,4.66

             1111111,唐口煤矿5306工作面,KJ615,2018-03-20 07:14:29,6,4.23

        自定义拦截器代码:

package cn.com.bonc.interceptor;


import cn.com.bonc.interceptor.utils.CustomTime;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * 应力监测数据过滤器,主要用来处理应力监测数据文件:<br>
 * 1. 将文件头数据去掉,然后添加到每行数据中;<br>
 * 2. 实现去掉空行数据功能;
 */
public class StressMonitorData implements Interceptor {

    //用于输出日志
    private Logger logger = LoggerFactory.getLogger(StressMonitorData.class);
    //用于将日志输出至单独的日志文件中去
    private Logger logger2 = LoggerFactory.getLogger("logger2");

    //文件有两行表头,第一行三个信息,第二行一个信息
    private String message11 = "";
    private String message12 = "";
    private String message13 = "";
    private String message21 = "";

    private String oldBody = "";
    private String newBody = "";

    private String currentFileName = "";

    @Override
    public void initialize() {

    }

    /**
     * 读取event body内容,检查是否为表头<br>
     * 如果为表头,则更新表头信息变量<br>
     * 如果为记录数据,则对其进行拼接
     *
     * @param event
     * @return
     */
    @Override
    public Event intercept(Event event) {
        String fileName = event.getHeaders().get("fileName");
        //将当前处理的文件名组装为日志输出至日志文件中
        if (!currentFileName.equals(fileName)) {
            currentFileName = fileName;
            logger2.info(CustomTime.getTime() + "------处理文件:" + currentFileName);
        }
        //清空body变量,将新的event body内容追加到body变量
        oldBody = new String(event.getBody());
        if (oldBody == "") {
            //如果该行为空,则直接舍弃
            return null;
        }
        String[] bodys = oldBody.split(";");
        int length = bodys.length;
        if (length == 3) {
            logger.info("------接收到第一行文件头,内容为:" + oldBody);
            //event内容为文件第一行内容
            message11 = bodys[0];
            message12 = bodys[1];
            message13 = bodys[2].split("\\|")[0];
            return null;
        } else if (length == 1) {
            logger.info("------接收到第二行文件头,内容为:" + oldBody);
            //event内容为文件第二行内容
            message21 = bodys[0].split("\\|")[0];
            return null;
        } else if (length == 2) {
            logger.info("------接收到记录数据,内容为:" + oldBody);
            //event内容为文件记录数据,将文件头信息和实际数据进行拼接
            newBody = message11 + "," + message12 + "," + message13 + "," + message21 + "," +
                    bodys[0] + "," + bodys[1].split("~")[0];
            event.setBody(newBody.getBytes());
            return event;
        } else {
            logger.info("------接收到脏数据,内容为:" + oldBody);
            //在上述三种情况之外的,一律按照脏数据处理,直接舍弃
            return null;
        }
    }

    @Override
    public List<Event> intercept(List<Event> events) {

        int i = 1;
        for (Event e : events) {
            logger.info("------原始数据第" + (i++) + "行内容为:" + new String(e.getBody()));
        }

        List<Event> result = new ArrayList<Event>();
        Event event;
        for (Event e : events) {
            event = intercept(e);
            if (event != null) {
                result.add(event);
            }
        }

        i = 1;
        for (Event e : result) {
            logger.info("------结果数据第" + (i++) + "行内容为:" + new String(e.getBody()));
        }

        return result;
    }

    @Override
    public void close() {

    }

    public static class Builder implements Interceptor.Builder {
        private Logger logger = LoggerFactory.getLogger(Builder.class);

        //使用Builder初始化自定义Interceptor
        @Override
        public Interceptor build() {
            logger.info("------初始化StressMonitorData拦截器");
            return new StressMonitorData();
        }

        @Override
        public void configure(Context context) {

        }
    }
}

        log4j.properties文件内容为:

log4j.rootLogger = INFO, out

log4j.appender.out = org.apache.log4j.ConsoleAppender
log4j.appender.out.layout = org.apache.log4j.PatternLayout
log4j.appender.out.layout.ConversionPattern = %d (%t) [%p - %l] %m%n

log4j.logger.org.apache.flume = DEBUG

log4j.logger.logger2=debug,appender2
log4j.appender.appender2=org.apache.log4j.FileAppender
log4j.appender.appender2.File=handelFile.log
log4j.appender.appender2.layout=org.apache.log4j.TTCCLayout

      上面的logger2可以将日志输出至指定的日志文件中,已便于将不同的日志输出至不同的文件中保存,方便后续分析。logger2初始化代码:

private Logger logger2 = LoggerFactory.getLogger("logger2");

    为了方便测试,我打印了很多log日志,在实际生产环境下,可以去掉不必要的日志。

    3. 这个功能我直接演示正则表达式,这是最好的方法,但也是比较难得方法,毕竟正则表达式能看懂,但不一定会写。

        自定义过滤器代码示例:

package cn.com.bonc.interceptor;

import cn.com.bonc.interceptor.utils.CustomTime;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 获取拦截器正则表达式参数,用来匹配正则表达式来修改文件名:<br>
 * 1. 匹配正则表达式,修改event头信息中的文件名;<br>
 * 2. 去除文件数据中的空行数据;
 */
public class HeaderParamRegular implements Interceptor {



    //拦截器参数,文件名要匹配的正则表达式
    private static String regex;
    //拦截器参数,文件名匹配之后要修改的字符串
    private static String replace;
    private String currentFileName = "";

    private Logger logger = LoggerFactory.getLogger(HeaderParamRegular.class);
    //logger用于将日志输出至指定的文件中
    private Logger logger2 = LoggerFactory.getLogger("logger2");

    @Override
    public void initialize() {

    }

    /**
     * 处理文件头中的文件名信息
     *
     * @param event event对象
     * @return 处理完之后的event对象
     */
    @Override
    public Event intercept(Event event) {
        Map<String, String> headers = event.getHeaders();
        String fileName = headers.get("fileName");
        //将当前处理的文件名组合为日志输出至单独的日志文件中去
        if (!currentFileName.equals(fileName)){
            currentFileName = fileName;
            logger2.info(CustomTime.getTime() + "------处理文件:" + currentFileName);
            if ("".equals(new String(event.getBody()))){
                return null;
            }
        }
        String[] regexs = regex.split(",");
        int length = regexs.length;
        String[] replaces = replace.split(",");
        boolean flag = false;
        if (fileName != null) {
            logger.info("------读取到event header中的fileName信息为:" + fileName);
            for (int i = 0; i < length; i++) {
                if (fileName.matches(regexs[i])) {
                    headers.put("fileName", replaces[i]);
                    event.setHeaders(headers);
                    flag = true;
                    break;
                }
            }
            if (!flag) {
                logger.info("------fileName未匹配到合适的regex信息,event头中文件名保持不变。");
            }
        } else {
            logger.info("------event header中没有fileName信息");
        }
        return event;
    }

    @Override
    public List<Event> intercept(List<Event> events) {
        List<Event> results = new ArrayList<>();
        Event event;
        for (Event e : events) {
            event = intercept(e);
            if (event != null) {
                results.add(event);
            }
        }
        return results;
    }

    @Override
    public void close() {

    }


    public static class Builder implements Interceptor.Builder {

        private Logger logger = LoggerFactory.getLogger(HeaderParamRegular.class);

        //使用Builder初始化自定义Interceptor
        @Override
        public Interceptor build() {
            logger.info("------初始化HeaderParam拦截器");
            return new HeaderParamRegular();
        }

        //通过该方法中的context对象,可以获取到flume配置文件中配置的参数值,并且先于build方法执行!!!
        @Override
        public void configure(Context context) {
            regex = context.getString("regex");
            replace = context.getString("replace");
            logger.info("------获取到拦截器参数pattern为:" + regex);
            logger.info("------获取到拦截器参数replace为:" + replace);
        }
    }


}

        flume配置代码示例:

#source直接删除原始文件,解决重名问题


#agent
test.sources=r1
test.channels=c1
test.sinks=s1

#source
test.sources.r1.type=spooldir
test.sources.r1.spoolDir=/home/jumpserver/flume/data
test.sources.r1.consumeOrder=youngest
test.sources.r1.recursiveDirectorySearch=true
test.sources.r1.deletePolicy=immediate
test.sources.r1.pollDelay=500
test.sources.r1.basenameHeader=true
test.sources.r1.basenameHeaderKey=fileName
#source-interceptor
test.sources.r1.interceptors=i1
test.sources.r1.interceptors.i1.type=cn.com.bonc.interceptor.HeaderParamRegular$Builder
test.sources.r1.interceptors.i1.regex=^test1_\\w+\\.txt$,^test2_\\w+\\.txt$
test.sources.r1.interceptors.i1.replace=test1,test2

#channel
test.channels.c1.type=memory
test.channels.c1.capacity=1000
test.channels.c1.transactionCapacity=100

#sink
test.sinks.s1.type=logger

#package
test.sources.r1.channels=c1
test.sinks.s1.channel=c1

    该自定义拦截器对文件名进行修改,根据regex和replace参数值,一一对应。如果文件名匹配上了regex中的第一个正则表达式,则将其改为replace中的第一个字符串;以此类推。自定义拦截器代码以及flume配置文件代码自行查看。

            直接运行,然后往/home/jumpserver/flume/data目录中拷贝文件,观察运行结果;

                 

                上图显示了将要拷贝的两个文件的实际内容和文件名。

                 

     上图为flume启动运行结果。

                 

                上图为往/home/jumpserver/flume/data目录中拷贝文件。              

     上图为flume自定义过滤器处理结果,可以看到自定义过滤器已经将event头信息中的文件名根据正则表达式匹配将其改为我们需要的文件名。

                 

                上图为往/home/jumpserver/flume/data目录中拷贝文件,这次拷贝的文件,文件名已经发生了变化,文件内容也不同。               

     上图为flume自定义过滤器处理结果,可以看到自定义过滤器已经将event头信息中的文件名根据正则表达式匹配将其改为我们需要的文件名。

            PS:

     该自定义拦截器的flume配置,我只是将处理结果打印出来了,并没有按照不同的文件名将其输出至不同的HDFS路径或者是其他类型sink不同的路径,大家可以参考flume官网文档说明,具体配置selector=multiplexing即可。

5. 上点干货

    flume启动命令:

        flume-ng agent -c ./ -f test.conf -n test -Dflume.monitoring.type=http -Dflume.monitoring.port=65000

    flume关闭命令:

        ps -x | grep 65000 | grep -v grep| cut -d " " -f 1 | xargs kill -2

 这样开启和关闭flume,可以通过端口号来区分一台机器上启动的多个flume agent示例,十分方便。

    具体都是什么意思,大家自己百度即可。

猜你喜欢

转载自blog.csdn.net/u012443641/article/details/80757229