log4j+flume+kafka管理日志,查询日志

来源:互联网 发布:node.js 教程 编辑:程序博客网 时间:2024/05/19 05:01

由于系统每天生成日志文件非常庞大所以做了这个日志分类查询功能。

先说明一下原理:

1、log4j负责在系统中收集日志,log4j的配置和生成日志的规则要定好

2、flume负责监控log4j生成日志的文件夹,并进行分类(将debug、error等分开存)

3、kafka负责存储和查询

 

log4j的日志格式:

$ERROR$ [2014-08-14 10:51:21],10.185.235.85,/cnpc-vms/aaa,ServerName,zzzzz

第一个$中间表示这个日志是什么类型的。

第二个[]中间的表示生成日志的时间

前两个用做存储和查询条件,后面用逗号隔开的部分是服务器信息IP,属于哪个服务,服务名称,ZZZ表示具体日志信息

 

flume的配置

#fks : jakiro flume kafka storm integration
fks.sources=source1
fks.sinks=sink1
fks.channels=channel1

# configure source1
fks.sources.source1.type=spooling  #(*1这个配置表示监听的方式)
fks.sources.source1.command=tail -F/var/log/apache/flumeSpool

# configure sink1
fks.sinks.sink1.type=test.KafkaSink #(*2这位置是配置一个flume的扩展类,在这里可把日志分类)

# configure channel1
fks.channels.channel1.type=file
fks.channels.channel1.checkpointDir=/usr/flume/checkpointdir/tcpdir/example_fks_001
fks.channels.channel1.dataDirs=/usr/flume/datadirs/tddirs/example_fks_001

# bind source and sink
fks.sources.source1.channels=channel1
fks.sinks.sink1.channel=channel1

做标记*1的位置可以换很多方式,我这里的监控文件夹里的新文件,当这个文件夹有新文件生成的时候flume就会读取这个文件

更多的方式访问阿帕奇官网

http://flume.apache.org/FlumeUserGuide.html#flume-channels

*2 这个位置是配置一个扩展类,把flume读取过来的数据分类存储到kafka中

自己随便建一个java项目,我这里叫kafka-flume,写完之后打成jar包把你的包名配置在上面就行了,包名叫什么无所谓

下面是这个类的具体的代码

package test; import java.util.Properties; import kafka.javaapi.producer.Producer; import kafka.producer.KeyedMessage; import kafka.producer.ProducerConfig; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.flume.Channel; import org.apache.flume.Context; import org.apache.flume.Event; import org.apache.flume.EventDeliveryException; import org.apache.flume.Sink; import org.apache.flume.Transaction; import org.apache.flume.conf.Configurable; import org.apache.flume.sink.AbstractSink; public class KafkaSink extends AbstractSink implements Configurable {  private static final Log logger = LogFactory.getLog(KafkaSink.class);  private String topic;  private Producer<String, String> producer;  @Override  public void configure(Context arg0) {   this.topic = "DEFAULT";   Properties props = new Properties();   props.setProperty("metadata.broker.list", "Slave1.node:9092");   props.setProperty("serializer.class", "kafka.serializer.StringEncoder");   props.put("zookeeper.connect", "Slave1.node:2222,Slave2.node:2222,Slave3.node:2222");   // props.setProperty("num.partition", "5");   props.put("request.required.acks", "1");   ProducerConfig config = new ProducerConfig(props);   this.producer = new Producer(config);   logger.info("=====================================================");   logger.info("KafkaSink initialize is successful.");  }  @Override  public Sink.Status process() throws EventDeliveryException {   Channel channel = getChannel();   Transaction tx = channel.getTransaction();   try {    tx.begin();    Event e = channel.take();    if (e == null) {     tx.rollback();     Status localStatus = Sink.Status.BACKOFF;     return localStatus;    }    logger.info("no file :" + new String(e.getBody()));    KeyedMessage<String, String> data = null;    //这个message就是从日志文件中读取的内容    String message = new String(e.getBody());    int typeStart = message.indexOf("$");    int typeEnd = message.lastIndexOf("$") + 1;    String MessageType = "";    //这个位置把日志标志位判断一下,存到不同的topic中,这样方便后面的查询    if (typeStart > 0 && typeEnd > 0) {     MessageType = message.substring(typeStart, typeEnd);     if ("$WARN$".equals(MessageType)) {      data = new KeyedMessage("WARNING", message);     } else if ("$ERROR$".equals(MessageType)) {      data = new KeyedMessage("ERROR", message);     } else if ("$INFO$".equals(MessageType)) {      data = new KeyedMessage("INFO", message);     } else if ("$DUBUG$".equals(MessageType)) {      data = new KeyedMessage("$DUBUG$", message);     } else {      data = new KeyedMessage(this.topic, message);     }     this.producer.send(data);     logger.info("Flume message to kafka : {}" + message);     tx.commit();     Status localStatus = Sink.Status.READY;     return localStatus;    } else {     data = new KeyedMessage(this.topic, message);     this.producer.send(data);     tx.commit();     Status localStatus = Sink.Status.READY;     return localStatus;    }   } catch (Exception e) {    logger.error("Flume KafkaSinkException: { }", e);    tx.rollback();    Sink.Status localStatus = Sink.Status.BACKOFF;    return localStatus;   } finally {    tx.close();   }  } }

 

至此就可以把日志分类了,这里可能需要一些包,大家自行下载吧!

 

下面说一下kafka的查询

我接触了两种查询,一种是只读取未读取过的日志,类似于socket那样一直有一个阻塞线程等待接收数据。另一种是读取topic中所有的数据。

但是这两种查询方式不能满足我的需求,所以我在第二种查询上做了改动,改成查询一个区间的offset数据。这里需要的offset值呢是在自己的工程里有个日志分析的系统在存储log4j的时候做一下记录,每10分钟记录一下offset存储到数据库中,这里只存储时间和ofseet值oracle或者mysql都能承受住。查询的时候先查询数据库获取到offset值,再利用这个日志的区间查出我们要的日志。

先看一下改过的查询方法。

package test2;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import kafka.api.FetchRequest;
import kafka.api.FetchRequestBuilder;
import kafka.api.PartitionOffsetRequestInfo;
import kafka.common.TopicAndPartition;
import kafka.javaapi.FetchResponse;
import kafka.javaapi.OffsetResponse;
import kafka.javaapi.PartitionMetadata;
import kafka.javaapi.TopicMetadata;
import kafka.javaapi.TopicMetadataRequest;
import kafka.javaapi.consumer.SimpleConsumer;
import kafka.message.MessageAndOffset;

/**
 * Kafka官网给的案例 SimpleConsumer,我在上面改了一下
 *
 * @Author zhouyu
 * @Time 2014-06-29 15:09:21
 *
 */
public class TestSimpleConsumer {
 public static void main(String args[]) {
  TestSimpleConsumer example = new TestSimpleConsumer();

  // long maxReads = 100;
  String topic = "ERROR";
  int partition = 0;

  // 用上面那条命令创建topic的时候有个--partitions
  // 5这样的参数,说明为该topic指定了5个分区,然后再Consumer端,也就是这个类从topic读取数据的时候,这个类里只能一个分
  // 区一个分区的读取数据,因为有5个分区,默认是从0开始,也就是1~4,然后我们从Producer的产生的数据会随机Hash到这5个分区中去,(中间过程省略,不解释
  // 、、、),然后Consumer
  // 这边会读取数据,也就是说你想要看Producer产生的所有数据,也就是你需要要看每个分区的数据了。(一开始指定分区数为1也行)

  List<String> seeds = new ArrayList<String>();
  seeds.add("Slave1.node");
  int port = Integer.parseInt("9092");
  try {
   example.run(topic, partition, seeds, port);
  } catch (Exception e) {
   System.out.println("Oops:" + e);
   e.printStackTrace();
  }
 }

 private List<String> m_replicaBrokers = new ArrayList<String>();
 private long readOffset = 1L;// 起始的offset值
 private long endOffset = 200L;// 结束的offset值

 public TestSimpleConsumer() {
  m_replicaBrokers = new ArrayList<String>();
 }

 public void run(String a_topic, int a_partition, List<String> a_seedBrokers, int a_port) throws Exception {
  PartitionMetadata metadata = findLeader(a_seedBrokers, a_port, a_topic, a_partition);

  String leadBroker = metadata.leader().host();
  String clientName = "Client_" + a_topic + "_" + a_partition;

  SimpleConsumer consumer = new SimpleConsumer(leadBroker, a_port, 100000, 64 * 1024, clientName);
  //这里的getLastOffset可以取出最后的offset值,可以在查询实时数据时用到,我这里没用他,但是写先写上,可以扩展
  long lastOffset = getLastOffset(consumer, a_topic, a_partition,kafka.api.OffsetRequest.EarliestTime(), clientName);
  
  FetchRequest req = new FetchRequestBuilder().clientId(clientName).addFetch(a_topic, a_partition, readOffset, 100000).build();
  FetchResponse fetchResponse = consumer.fetch(req);

  Iterator<MessageAndOffset> iterator = fetchResponse.messageSet(a_topic, a_partition).iterator();
  MessageAndOffset messageAndOffset = null;
  for (long i = 0; i < endOffset - readOffset + 1; i++) {
   if (iterator.hasNext()) {
    messageAndOffset = iterator.next();
   }
   long currentOffset = messageAndOffset.offset();
   if (currentOffset > endOffset) {
    break;
   } else {
    ByteBuffer payload = messageAndOffset.message().payload();

    byte[] bytes = new byte[payload.limit()];
    payload.get(bytes);
    System.out.println(String.valueOf(messageAndOffset.offset()) + ": " + new String(bytes, "UTF-8"));
   }
  }
  if (consumer != null)
   consumer.close();
 }

 public static long getLastOffset(SimpleConsumer consumer, String topic, int partition, long whichTime, String clientName) {
  TopicAndPartition topicAndPartition = new TopicAndPartition(topic, partition);
  Map<TopicAndPartition, PartitionOffsetRequestInfo> requestInfo = new HashMap<TopicAndPartition, PartitionOffsetRequestInfo>();
  requestInfo.put(topicAndPartition, new PartitionOffsetRequestInfo(whichTime, 1));
  kafka.javaapi.OffsetRequest request = new kafka.javaapi.OffsetRequest(requestInfo, kafka.api.OffsetRequest.CurrentVersion(), clientName);
  OffsetResponse response = consumer.getOffsetsBefore(request);

  if (response.hasError()) {
   System.out.println("Error fetching data Offset Data the Broker. Reason: " + response.errorCode(topic, partition));
   return 0;
  }
  long[] offsets = response.offsets(topic, partition);
  return offsets[0];
 }

 private PartitionMetadata findLeader(List<String> a_seedBrokers, int a_port, String a_topic, int a_partition) {
  PartitionMetadata returnMetaData = null;
  loop: for (String seed : a_seedBrokers) {
   SimpleConsumer consumer = null;
   try {
    consumer = new SimpleConsumer(seed, a_port, 100000, 64 * 1024, "leaderLookup");
    List<String> topics = Collections.singletonList(a_topic);
    TopicMetadataRequest req = new TopicMetadataRequest(topics);
    kafka.javaapi.TopicMetadataResponse resp = consumer.send(req);

    List<TopicMetadata> metaData = resp.topicsMetadata();
    for (TopicMetadata item : metaData) {
     for (PartitionMetadata part : item.partitionsMetadata()) {
      if (part.partitionId() == a_partition) {
       returnMetaData = part;
       break loop;
      }
     }
    }
   } catch (Exception e) {
    System.out.println("Error communicating with Broker [" + seed + "] to find Leader for [" + a_topic + ", " + a_partition
      + "] Reason: " + e);
   } finally {
    if (consumer != null)
     consumer.close();
   }
  }
  if (returnMetaData != null) {
   m_replicaBrokers.clear();
   for (kafka.cluster.Broker replica : returnMetaData.replicas()) {
    m_replicaBrokers.add(replica.host());
   }
  }
  return returnMetaData;
 }
}

这样就可以查询一个区间的数据了,然后可以做一个servlet的简单页面让运维人人员填上查询日志的类型、时间段,展示出来最后提供一个下载的功能把日志给他,可以肉眼分析了。总比运维人员去服务器上把几个G 的log日志下载下来然后用好长时间再打开会好用很多。这里主要是一些思路有不足之处请大家之处,谢谢!

 

0 0