日志系统_日志系统:缓冲区处理 - CSDN
精华内容
参与话题
  • 日志管理系统

    千次阅读 2019-06-27 21:45:10
    为什么需要日志管理系统 保留现场 自知者自明 所有即将发生的,都取决于已经发生的 数据商业化运作 1.1 日志管理系统的解决方案 机器上的日志实时收集,存储到日志中心 给日志建立索引,通过索引能很快找到...

    一.为什么需要日志管理系统

    1. 保留现场
    2. 自知者自明
    3. 所有即将发生的,都取决于已经发生的
    4. 数据商业化运作

    1.1 日志管理系统的解决方案

    1. 机器上的日志实时收集,存储到日志中心
    2. 给日志建立索引,通过索引能很快找到日志
    3. 架设web界面,在web上完成日志的搜索

    1.2 日志管理系统的困难

    1. 日志量很大,每天几十亿条
    2. 日志的实时收集,延迟控制在分钟级别
    3. 能够在线水平扩展

    1.3 业内解决方案-ELK

         ELK 不是一款软件,而是 Elasticsearch、Logstash 和 Kibana 三种软件产品的首字母缩写:

    • Elasticsearch:分布式搜索和分析引擎,具有高可伸缩、高可靠和易管理等特点。基于 Apache Lucene 构建,能对大容量的数据进行接近实时的存储、搜索和分析操作。通常被用作某些应用的基础搜索引擎,使其具有复杂的搜索功能

    • Logstash:数据收集引擎。它支持动态的从各种数据源搜集数据,并对数据进行过滤、分析、丰富、统一格式等操作,然后存储到用户指定的位置

    • Kibana:数据分析和可视化平台。通常与 Elasticsearch 配合使用,对其中数据进行搜索、分析和以统计图表的方式展示

    1.3.1 简单ELK架构

          把一个 Logstash 数据搜集节点扩展到多个,分布于多台机器,将解析好的数据发送到 Elasticsearch server 进行存储,最后在 Kibana 查询、生成日志报表等 。

    1.3.2 引入消息队列的ELK架构

          使用 Logstash 从各个数据源搜集数据,然后经消息队列输出插件输出到消息队列中。目前 Logstash 支持 Kafka、Redis、RabbitMQ 等常见消息队列。然后 Logstash 通过消息队列输入插件从队列中获取数据,分析过滤后经输出插件发送到 Elasticsearch,最后通过 Kibana 展示。

    1.3.3 ELK方案的问题

    • 运维成本高,每增加一个日志收集,需要手动修改配置
    • 监控缺失,无法准确获取Logstash的状态二. 日志管理系统架构

    二. 日志收集系统架构

    2.1 组件介绍

    •  Log Agent:日志收集客户端,用来收集服务器上的日志
    •  Kafka:基于zookeeper协调的高吞吐量的分布式队列
    •  ES:elasticsearch,开源的分布式搜索引擎,给文档建立索引
    •  Hadoop:分布式计算框架,能够对大量数据进行分布式处理的平台

    2.2.1 日志采集工具介绍

                                                                Logstash

    优势:

           Logstash 主要的有点就是它的灵活性,主要因为它有很多插件,详细的文档以及直白的配置格式让它可以在多种场景下应用。我们基本上可以在网上找到很多资源,几乎可以处理任何问题。

    劣势:

            Logstash 致命的问题是它的性能以及资源消耗(默认的堆大小是 1GB)。尽管它的性能在近几年已经有很大提升,与它的替代者们相比还是要慢很多的。这里有 Logstash 与 rsyslog 性能对比以及Logstash 与 filebeat 的性能对比。它在大数据量的情况下会是个问题。另一个问题是它目前不支持缓存,目前的典型替代方案是将 Redis 或 Kafka 作为中心缓冲池:

    典型应用场景:

           因为 Logstash 自身的灵活性以及网络上丰富的资料,Logstash 适用于原型验证阶段使用,或者解析非常的复杂的时候。在不考虑服务器资源的情况下,如果服务器的性能足够好,我们也可以为每台服务器安装 Logstash 。我们也不需要使用缓冲,因为文件自身就有缓冲的行为,而 Logstash 也会记住上次处理的位置。如果服务器性能较差,并不推荐为每个服务器安装 Logstash ,这样就需要一个轻量的日志传输工具,将数据从服务器端经由一个或多个 Logstash 中心服务器传输到 Elasticsearch:随着日志项目的推进,可能会因为性能或代价的问题,需要调整日志传输的方式(log shipper)。当判断 Logstash 的性能是否足够好时,重要的是对吞吐量的需求有着准确的估计,这也决定了需要为 Logstash 投入多少硬件资源。

                                                                 Filebeat

    优势:

           Filebeat 只是一个二进制文件没有任何依赖。它占用资源极少,尽管它还十分年轻,正式因为它简单,所以几乎没有什么可以出错的地方,所以它的可靠性还是很高的。它也为我们提供了很多可以调节的点,例如:它以何种方式搜索新的文件,以及当文件有一段时间没有发生变化时,何时选择关闭文件句柄。

    劣势:

          Filebeat 的应用范围十分有限,所以在某些场景下我们会碰到问题。例如,如果使用 Logstash 作为下游管道,我们同样会遇到性能问题。正因为如此,Filebeat 的范围在扩大。开始时,它只能将日志发送到 Logstash 和 Elasticsearch,而现在它可以将日志发送给 Kafka 和 Redis,在 5.x 版本中,它还具备过滤的能力。

    典型应用场景:

          Filebeat 在解决某些特定的问题时:日志存于文件,我们希望将日志直接传输存储到 Elasticsearch。这仅在我们只是抓去(grep)它们或者日志是存于 JSON 格式(Filebeat 可以解析 JSON)。或者如果打算使用 Elasticsearch 的 Ingest 功能对日志进行解析和丰富。将日志发送到 Kafka/Redis。所以另外一个传输工具(例如,Logstash 或自定义的 Kafka 消费者)可以进一步丰富和转发。这里假设选择的下游传输工具能够满足我们对功能和性能的要求。

                                                                 Logagent

    优势:

           可以获取 /var/log 下的所有信息,解析各种格式(Elasticsearch,Solr,MongoDB,Apache HTTPD等等),它可以掩盖敏感的数据信息,例如,个人验证信息(PII),出生年月日,信用卡号码,等等。它还可以基于 IP 做 GeoIP 丰富地理位置信息(例如,access logs)。同样,它轻量又快速,可以将其置入任何日志块中。在新的 2.0 版本中,它以第三方 node.js 模块化方式增加了支持对输入输出的处理插件。重要的是 Logagent 有本地缓冲,所以不像 Logstash ,在数据传输目的地不可用时会丢失日志。

    劣势:

          尽管 Logagent 有些比较有意思的功能(例如,接收 Heroku 或 CloudFoundry 日志),但是它并没有 Logstash 灵活。

    典型应用场景:

          Logagent 作为一个可以做所有事情的传输工具是值得选择的(提取、解析、缓冲和传输)。

     

                                                            tailf示例:

    package main
    
    import (
    	"fmt"
    	"github.com/hpcloud/tail"
    	"time"
    )
    func main() {
    	filename := "./my.log"
    	tails, err := tail.TailFile(filename, tail.Config{
    		ReOpen:    true,
    		Follow:    true,
    		Location:  &tail.SeekInfo{Offset: 0, Whence: 2},
    		MustExist: false,
    		Poll:      true,
    	})
    	if err != nil {
    		fmt.Println("tail file err:", err)
    		return
    	}
    	var msg *tail.Line
    	var ok bool
    	for true {
    		msg, ok = <-tails.Lines
    		if !ok {
    			fmt.Printf("tail file close reopen, filename:%s\n", tails.Filename)
    			time.Sleep(100 * time.Millisecond)
    			continue
    		}
    		fmt.Println("msg:", msg)
    	}
    }
    

    2.2.2 Kafka介绍

    介绍:

            Kafka是最初由Linkedin公司开发,是一个分布式、支持分区的(partition)、多副本的(replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、storm/Spark流式处理引擎,web/nginx日志、访问日志,消息服务等等,用scala语言编写,Linkedin于2010年贡献给了Apache基金会并成为顶级开源 项目。

    Kafka的特性:

           高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。

    • 可扩展性:kafka集群支持热扩展
    • 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
    • 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)
    • 高并发:支持数千个客户端同时读写

    Kafka的应用场景:

    • 异步处理:把非关键流程异步化,提高系统的响应时间和健壮性

     

    • 应用解耦:通过消息队列

    • 流量削峰:缓存消息

     

    Kafka的原理:

            两个服务器Kafka群集,托管四个分区(P0-P3),包含两个使用者组。消费者组A有两个消费者实例,B组有四个消费者实例。每个实例(server)成为broker。无论是kafka集群,还是producer和consumer都依赖于zookeeper来保证系统可用性集群保存一些meta信息。

     

     

          一个Topic可以认为是一类消息,每个topic将被分成多个partition(区),每个partition在存储层面是append log文件。任何发布到此partition的消息都会被直接追加到log文件的尾部,每条消息在文件中的位置称为offset(偏移量),offset为一个long型数字,它是唯一标记一条消息。它唯一的标记一条消息。kafka并没有提供其他额外的索引机制来存储offset,因为在kafka中几乎不允许对消息进行“随机读写”。

    Kafka使用示例:

    package main
    
    import (
    	"fmt"
    	"github.com/Shopify/sarama"
    )
    
    func main() {
    	config := sarama.NewConfig()
    	config.Producer.RequiredAcks = sarama.WaitForAll
    	config.Producer.Partitioner = sarama.NewRandomPartitioner
    	config.Producer.Return.Successes = true
    
    	msg := &sarama.ProducerMessage{}
    	msg.Topic = "nginx_log"
    	msg.Value = sarama.StringEncoder("this is a good test, my message is good")
    
    	client, err := sarama.NewSyncProducer([]string{"192.168.31.177:9092"}, config)
    	if err != nil {
    		fmt.Println("producer close, err:", err)
    		return
    	}
    
    	defer client.Close()
    
    	pid, offset, err := client.SendMessage(msg)
    	if err != nil {
    		fmt.Println("send message failed,", err)
    		return
    	}
    
    	fmt.Printf("pid:%v offset:%v\n", pid, offset)
    }
    

    2.2.3 Elasticsearch介绍

    介绍:

            Elasticsearch(ES)是一个基于Lucene构建的开源、分布式、RESTful接口的全文搜索引擎。Elasticsearch还是一个分布式文档数据库,其中每个字段均可被索引,而且每个字段的数据均可被搜索,ES能够横向扩展至数以百计的服务器存储以及处理PB级的数据。可以在极短的时间内存储、搜索和分析大量的数据。

    Near Realtime(NRT) 几乎实时

    Elasticsearch是一个几乎实时的搜索平台。意思是,从索引一个文档到这个文档可被搜索只需要一点点的延迟,这个时间一般为毫秒级。

    Cluster 集群

           群集是一个或多个节点(服务器)的集合, 这些节点共同保存整个数据,并在所有节点上提供联合索引和搜索功能。一个集群由一个唯一集群ID确定,并指定一个集群名(默认为“elasticsearch”)。该集群名非常重要,因为节点可以通过这个集群名加入群集,一个节点只能是群集的一部分。

           确保在不同的环境中不要使用相同的群集名称,否则可能会导致连接错误的群集节点。例如,你可以使用logging-dev、logging-stage、logging-prod分别为开发、阶段产品、生产集群做记录。

    Node节点

          节点是单个服务器实例,它是群集的一部分,可以存储数据,并参与群集的索引和搜索功能。就像一个集群,节点的名称默认为一个随机的通用唯一标识符(UUID),确定在启动时分配给该节点。如果不希望默认,可以定义任何节点名。这个名字对管理很重要,目的是要确定你的网络服务器对应于你的ElasticSearch群集节点。

           我们可以通过群集名配置节点以连接特定的群集。默认情况下,每个节点设置加入名为“elasticSearch”的集群。这意味着如果你启动多个节点在网络上,假设他们能发现彼此都会自动形成和加入一个名为“elasticsearch”的集群。

          在单个群集中,您可以拥有尽可能多的节点。此外,如果“elasticsearch”在同一个网络中,没有其他节点正在运行,从单个节点的默认情况下会形成一个新的单节点名为"elasticsearch"的集群。

    Index索引

           索引是具有相似特性的文档集合。例如,可以为客户数据提供索引,为产品目录建立另一个索引,以及为订单数据建立另一个索引。索引由名称(必须全部为小写)标识,该名称用于在对其中的文档执行索引、搜索、更新和删除操作时引用索引。在单个群集中,您可以定义尽可能多的索引。

    Type类型

           在索引中,可以定义一个或多个类型。类型是索引的逻辑类别/分区,其语义完全取决于您。一般来说,类型定义为具有公共字段集的文档。例如,假设你运行一个博客平台,并将所有数据存储在一个索引中。在这个索引中,您可以为用户数据定义一种类型,为博客数据定义另一种类型,以及为注释数据定义另一类型。

     Document文档

          文档是可以被索引的信息的基本单位。例如,您可以为单个客户提供一个文档,单个产品提供另一个文档,以及单个订单提供另一个文档。本文件的表示形式为JSON(JavaScript Object Notation)格式,这是一种非常普遍的互联网数据交换格式。在索引/类型中,您可以存储尽可能多的文档。请注意,尽管文档物理驻留在索引中,文档实际上必须索引或分配到索引中的类型。

    Shards & Replicas分片与副本

           索引可以存储大量的数据,这些数据可能超过单个节点的硬件限制。例如,十亿个文件占用磁盘空间1TB的单指标可能不适合对单个节点的磁盘或可能太慢服务仅从单个节点的搜索请求。为了解决这一问题,Elasticsearch提供细分你的指标分成多个块称为分片的能力。当你创建一个索引,你可以简单地定义你想要的分片数量。每个分片本身是一个全功能的、独立的“指数”,可以托管在集群中的任何节点。

    Shards分片的重要性主要体现在以下两个特征:

    • 分片允许您水平拆分或缩放内容的大小
    • 分片允许你分配和并行操作的碎片(可能在多个节点上)从而提高性能/吞吐量,这个机制中的碎片是分布式的以及其文件汇总到搜索请求是完全由ElasticSearch管理,对用户来说是透明的。

           在同一个集群网络或云环境上,故障是任何时候都会出现的,拥有一个故障转移机制以防分片和结点因为某些原因离线或消失是非常有用的,并且被强烈推荐。为此,Elasticsearch允许你创建一个或多个拷贝,你的索引分片进入所谓的副本或称作复制品的分片,简称Replicas。

    Replicas的重要性主要体现在以下两个特征:

    • 副本为分片或节点失败提供了高可用性。为此,需要注意的是,一个副本的分片不会分配在同一个节点作为原始的或主分片,副本是从主分片那里复制过来的。
    • 副本允许用户扩展你的搜索量或吞吐量,因为搜索可以在所有副本上并行执行。

     Elasticsearch代码示例:

    package main
    
    import (
    	"fmt"
    	elastic "gopkg.in/olivere/elastic.v2"
    )
    
    type Tweet struct {
    	User    string
    	Message string
    }
    
    func main() {
    	client, err := elastic.NewClient(elastic.SetSniff(false), elastic.SetURL("http://192.168.31.177:9200/"))
    	if err != nil {
    		fmt.Println("connect es error", err)
    		return
    	}
    
    	fmt.Println("conn es succ")
    
    	tweet := Tweet{User: "olivere", Message: "Take Five"}
    	_, err = client.Index().
    		Index("twitter").
    		Type("tweet").
    		Id("1").
    		BodyJson(tweet).
    		Do()
    	if err != nil {
    		// Handle error
    		panic(err)
    		return
    	}
    
    	fmt.Println("insert succ")
    }
    

     

    展开全文
  • java日志系统框架整理(转载)

    万次阅读 2018-02-06 11:45:38
    首先,在日志系统的森林里面理理头绪,修炼内功。参考文章如下: 1.https://www.cnblogs.com/gavanwanggw/p/7305218.html 2.https://www.cnblogs.com/gavanwanggw/p/7305218.html Java日志系统确实比较丰富,...

    参考:(方便记录,直接将内容贴过来了。如有侵权,请留言删除,此致敬意!)

    首先,在日志系统的森林里面理理头绪,修炼内功。参考文章如下:

    1.https://www.cnblogs.com/gavanwanggw/p/7305218.html

    2.https://www.cnblogs.com/gavanwanggw/p/7305218.html


    Java日志系统确实比较丰富,常用的有log4j、JUL、logback等等,同时伴随着日志系统的发展,出现了日志框架commons-logging和slf4j。

    简短地描述下日志发展,最先出现的是apache开源社区的log4j,这个日志确实是应用最广泛的日志工具,成为了java日志的事实上的标准。然而,当时Sun公司在jdk1.4中增加了JUL日志实现,企图对抗log4j,但是却造成了混乱,这个也是被人诟病的一点。当然也有其他日志工具的出现,这样必然造成开发者的混乱,因为这些日志系统互相没有关联,替换和统一也就变成了比较棘手的一件事。想象下你的应用使用log4j,然后使用了一个其他团队的库,他们使用了JUL,你的应用就得使用两个日志系统了,然后又有第二个库出现了,使用了simplelog。这个时候估计让你崩溃了,这是要闹哪样?这个状况交给你来想想办法,你该如何解决呢?进行抽象,抽象出一个接口层,对每个日志实现都适配或者转接,这样这些提供给别人的库都直接使用抽象层即可。不错,开源社区提供了commons-logging抽象,被称为JCL,也就是日志框架了,确实出色地完成了兼容主流的日志实现(log4j、JUL、simplelog),基本一统江湖,就连顶顶大名的spring也是依赖了JCL。看起来事物确实是美好,但是美好的日子不长,接下来另一个优秀的日志框架slf4j的加入导致了更加混乱的场面。比较巧的是slf4j的作者(Ceki Gülcü)就是log4j的作者,他觉得JCL不够优秀,所以他要自己搞一套更优雅的出来,于是slf4j日志体系诞生了,并为slf4j实现了一个亲子——logback,确实更加优雅,但是由于之前很多代码库已经使用JCL,虽然出现slf4j和JCL之间的桥接转换,但是集成的时候问题依然多多,对很多新手来说确实会很懊恼,因为比单独的log4j时代“复杂”多了,可以关注下这个,抱怨声确实很多。到此本来应该完了,但是Ceki Gülcü觉得还是得回头拯救下自己的“大阿哥”——log4j,于是log4j2诞生了,同样log4j2也参与到了slf4j日志体系中,想必将来会更加混乱。接下来详细解读日志系统的配合使用问题。

     

    JCL的实现原理,使用JCL一般(如果是log4j可以不需要)需要一个配置commons-logging.properties在classpath上,这个文件有一行代码:

    org.apache.commons.logging.LogFactory= org.apache.commons.logging.impl.LogFactoryImpl

     这个是告诉JCL使用哪个日志实现,JCL会在classpath下去加载对应的日志工厂实现类,具体的日志工厂实现类可以是log4j实现,可以是jul实现等等。用户只需要依赖JCL的api即可,对日志系统的替换只需要修改下commons-logging.properties文件切换到对应的日志工厂实现即可。但是我们也可以看到因为JCL是运行时去加载classpath下的实现类,会有classloader问题。而且因为log4j尚不支持参数占位符打日志的方式,所以JCL也会更加无力。

     

    slf4j的设计确实比较优雅,采用比较熟悉的方式——接口和实现分离,有个纯粹的接口层——slf4j-api工程,这个里边基本完全定义了日志的接口,所以对于开发来说,只需要使用这个即可。有接口就要有实现,比较推崇的实现是logback,logback完全实现了slf4j-api的接口,并且性能也比log4j更好,同时实现了变参占位符日志输出方式等等新特性。刚刚也提到log4j的使用比较普遍,所以支持这批用户依然是必须的,slf4j-log4j12也实现了slf4j-api,这个算是对log4j的适配器。同样推理,也会有对JUL的适配器slf4j-jdk14等等。为了使使用JCL等等其他日志系统后者实现的用户可以很简单地切换到slf4j上来,给出了各种桥接工程,比如:jcl-over-slf4j会把对JCL的调用都桥接到slf4j上来,可以看出jcl-over-slf4j的api和JCL是相同的,所以这两个jar是不能共存的。jul-to-slf4j是把对jul的调用桥接到slf4j上,log4j-over-slf4j是把对log4j的调用桥接到slf4j。下边用一个图来表示下这个家族的大致成员


     
     如上图,最上层表示桥阶层,下层表示具体的实现层,中间是接口层,可以看出
    这个图中所有的jar都是围绕着slf4j-api活动的,其中slf4j-jul的jar名称是slf4j-jdk14。slf4j-api和具体的实现层是怎么绑定的呢?这个其实是在编译时绑定的,这个可能不好理解,最直接的表达方式是不需要想jcl那样配置一下,只需要把slf4j-api和slf4j-log4j放到classpath上,即实现绑定。原理可以下载slf4j-api的源码查看,这个设计还是很巧妙的,slf4j-api中会去调用StaticLoggerBinder这个类获取绑定的工厂类,而每个日志实现会在自己的jar中提供这样一个类,这样slf4j-api就实现了编译时绑定实现。但是这样接口的源码编译需要依赖具体的实现了,不太合理吧?当时我也有这样的迷惑,因为打开slf4j-api的jar,看不到StaticLoggerBinder,就查看了slf4j-api的源码,在源码中看到了StaticLoggerBinder这个类,猜想应该是slf4j-api在打包过程中有动作,删除了自己保重的那个类,结果不出所料,确实是pom中的ant-task给处理了,pom中处理方式如下:

     

    Xml代码 
    1. <plugin>  
    2.         <groupId>org.apache.maven.plugins</groupId>  
    3.         <artifactId>maven-antrun-plugin</artifactId>  
    4.         <executions>  
    5.           <execution>  
    6.             <phase>process-classes</phase>  
    7.             <goals>  
    8.              <goal>run</goal>  
    9.             </goals>  
    10.           </execution>  
    11.         </executions>  
    12.         <configuration>  
    13.           <tasks>  
    14.             <echo>Removing slf4j-api's dummy StaticLoggerBinder and StaticMarkerBinder</echo>  
    15.             <delete dir="target/classes/org/slf4j/impl"/>  
    16.           </tasks>  
    17.         </configuration>  
    18.       </plugin>  

     

    打出来的slf4j-api是不完整的,只有找到包含StaticLoggerBinder这个类的包才可以,于是slf4j-log4j和logback-classic都提供了这个类。另外,slf4j-log4j和logback以及slf4j-jdk14是不能同时和slf4j共存的,也就是说只能有一个实现存在,不然启动会提示有多个绑定,判断多个实现的代码也很简单,如下:

     

    Java代码 
    1. private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";  
    2.   
    3.   private static void singleImplementationSanityCheck() {  
    4.     try {  
    5.       ClassLoader loggerFactoryClassLoader = LoggerFactory.class  
    6.           .getClassLoader();  
    7.       Enumeration paths;  
    8.       if (loggerFactoryClassLoader == null) {  
    9.         paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);  
    10.       } else {  
    11.         paths = loggerFactoryClassLoader  
    12.             .getResources(STATIC_LOGGER_BINDER_PATH);  
    13.       }  
    14.       // use Set instead of list in order to deal with  bug #138  
    15.       // LinkedHashSet appropriate here because it preserves insertion order during iteration  
    16.       Set implementationSet = new LinkedHashSet();  
    17.       while (paths.hasMoreElements()) {  
    18.         URL path = (URL) paths.nextElement();  
    19.         implementationSet.add(path);  
    20.       }  
    21.       if (implementationSet.size() > 1) {  
    22.         Util.report("Class path contains multiple SLF4J bindings.");  
    23.         Iterator iterator = implementationSet.iterator();  
    24.         while(iterator.hasNext()) {  
    25.           URL path = (URL) iterator.next();  
    26.           Util.report("Found binding in [" + path + "]");  
    27.         }  
    28.         Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");  
    29.       }  
    30.     } catch (IOException ioe) {  
    31.       Util.report("Error getting resources from path", ioe);  
    32.     }  
    33.   }  

     

    同时这个图中桥阶层和对应的实现jar是不能共存的,比如log4j-over-slf4j和slf4j-log4j,jul-to-slf4j和slf4j-jdk14,这个很好理解,会有死循环,启动也会报错。这种想象也就是说jar之前有互斥性,怎么使用maven有效解决“全局排除”会在以后的博文中讲解。jcl-over-slf4j是把对jcl的调用桥接到slf4j上,前文说到它和jcl是互斥的。图中的红线就表示互斥关系。

    当然slf4j也提供了可以把对slf4j的调用桥接到JCL上的工程包——slf4j-jcl,可以看出slf4j的设计者考虑非常周到,想想这样的情况:遗留系统使用的是JCL+log4j,因为系统功能演进,依赖了其他业务线的库,恰好那个库依赖了slf4j-api,并且应用需要关心这个库的日志,那么就需要转接日志到JCL上即可。细心的你可能一经发现,slf4j-jcl和jcl-over-slf4j也是互斥的,太多互斥的了:(。

    对于log4j2的加入,也很简单,和logback是很相似的,如下图:



     红线依然表示依赖的互斥,当然log4j-slf4j-impl也会和logback-classic、slf4j-log4j、slf4j-jdk14互斥。

     

    常见的问题:

    1.slf4j-api和实现版本不对应,尤其是1.6.x和1.5.x不兼容,如果没有特殊需求,直接升级到最新版本。

    2.slf4j的多个实现同时存在,比如slf4j-log4j和logback-classic,排除其中一个即可。

    3.log4j和logback不能同时使用?可以同时使用,这两个并不矛盾,遗留系统可能直接使用了log4j的代码,并且不能通过log4j-over-slf4j桥接,那么可以让他继续使用log4j,这里有详细的介绍。

    4.该如何选用这些呢?建议在非特殊情况下,都使用slf4j-api+logback,不要直接使用日志实现,性能没什么影响。对于要提供给别人的类库,建议使用slf4j-api,使用方可以自由选择具体的实现,并且建议类库不要依赖具体的日志实现。对于自己的桌面小应用,可以直接使用log4j,毕竟只是随便做做。

    5.logback因为木有spring提供的启动listener,所以要自己写?可以看看这里,开源社区已经做好了。

    6.日志系统一般不会影响到系统性能,除非你的系统对性能非常苛刻,如果这样你可以考虑使用Blitz4j,这个是Netflix社区对log4j的性能改进版,不过他们依然建议去使用log4j或者logback。


    内功修炼第二阶段:

    https://www.cnblogs.com/gavanwanggw/p/7305218.html


    1、概述

    眼下java应用日志收集都是採用日志框架(slf4j、apache commons logging)+日志系统(log4j、log4j2、LogBack、JUL等)的方式。而针对在分布式环境须要实时分析统计的日志,一般採用apache flume、facebook scribe等分布式日志收集系统。

    日志框架:提供日志调用的接口,实际的日志输出托付给日志系统实现。

    • JCL(Jakarta Commons Logging):比較流行的日志框架,非常多框架都依赖JCL,比如Spring等。
    • SLF4j:提供新的API,初衷是配合Logback使用,但同一时候兼容Log4j。

    日志系统:负责输出日志

    • Log4j:经典的一种日志解决方式。内部把日志系统抽象封装成Logger 、appender 、pattern 等实现。我们能够通过配置文件轻松的实现日志系统的管理和多样化配置。
    • Log4j2:Log4j的2.0版本号。对Log4j进行了优化。比方支持參数API、支持异步appender、插件式架构等
    • Logback:Log4j的替代产品。须要配合日志框架SLF4j使用
    • JUL(java.util.logging):JDK提供的日志系统。较混乱,不经常使用

    眼下我们的应用大部分都是使用了SLF4j作为门面,然后搭配log4j或者log4j2日志系统。



    以下将介绍slf4j + Log4j2 日志组件的引入、以及配置和使用

    2、Maven依赖

    </pre><pre name="code" class="html"><dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.13</version>
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-slf4j-impl</artifactId>
                <version>2.4.1</version>
            </dependency>
            <!--兼容log4j-->
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-1.2-api</artifactId>
                <version>2.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-api</artifactId>
                <version>2.4.1</version>
            </dependency>
            <dependency>
                <groupId>org.apache.logging.log4j</groupId>
                <artifactId>log4j-core</artifactId>
                <version>2.4.1</version>
            </dependency>
            <!--log4j2 异步appender须要-->
            <dependency>
                <groupId>com.lmax</groupId>
                <artifactId>disruptor</artifactId>
                <version>3.2.0</version>
            </dependency>

    3、配置
    • Appenders:也被称为Handlers。负责将日志事件记录到目标位置。在将日志事件输出之前,Appenders使用Layouts来对事件进行格式化处理。
    • Layouts:也被称为Formatters,它负责对日志事件中的数据进行转换和格式化。Layouts决定了数据在一条日志记录中的终于形式。
    • Loggers:Logger负责捕捉事件并将其发送给合适的Appender。

    当Logger记录一个事件时。它将事件转发给适当的Appender。

    然后Appender使用Layout来对日志记录进行格式化,并将其发送给控制台、文件或者其他目标位置。另外。Filters能够让你进一步指定一个Appender能否够应用在一条特定的日志记录上。在日志配置中,Filters并非必需的,但能够让你更灵活地控制日志消息的流动。

    3.1 Appender

    3.1.1 ConsoleAppender

    ConsoleAppender是最经常使用的Appenders之中的一个。它仅仅是将日志消息显示到控制台上。

    很多日志框架都将其作为默认的Appender。而且在主要的配置中进行预配置。

    比如,在Log4j中ConsoleAppender的配置參数例如以下所看到的。


    參数

    描写叙述

    filter

    用于决定是否须要使用该Appender来处理日志事件

    layout

    用于决定怎样对日志记录进行格式化,默认情况下使用“%m%n”。它会在每一行显示一条日志记录

    follow

    用于决定Appender是否须要了解输出(system.out或者system.err)的变化,默认情况是不须要跟踪这样的变化

    name

    用于设置Appender的名字

    ignoreExceptions

    用于决定是否须要记录在日志事件处理过程中出现的异常

    target

    用于指定输出目标位置。默认情况下使用SYSTEM_OUT。但也能够改动成SYSTEM_ERR

    <!--这个输出控制台的配置-->
            <Console name="Console" target="SYSTEM_OUT">
                <!--控制台仅仅输出level及以上级别的信息(onMatch)。其它的直接拒绝(onMismatch)-->
                <ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
                <!--这个都知道是输出日志的格式-->
                <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
            </Console>


    3.1.2 FileAppender

    FileAppenders将日志记录写入到文件里。它负责打开、关闭文件。向文件里追加日志记录,并对文件进行加锁。以免数据被破坏或者覆盖。

    在Log4j中,假设想创建一个FileAppender,须要指定目标文件的名字。写入方式是追加还是覆盖。以及是否须要在写入日志时对文件进行加锁:

     <File name="File" fileName="fileAppender.log" append="true" locking="true">
                <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
            </File>


    3.1.3 RollingFileAppender

    RollingFileAppender跟FileAppender的基本使用方法一样。

    但RollingFileAppender能够设置log文件的size(单位:KB/MB/GB)上限、数量上限,当log文件超过设置的size上限,会自己主动被压缩。

    RollingFileAppender能够理解为滚动输出日志,假设log4j 2记录的日志达到上限,旧的日志将被删除,腾出的空间用于记录新的日志。

    <!--这个会打印出全部的信息。每次大小超过size,则这size大小的日志会自己主动存入按年份-月份建立的目录以下并进行压缩,作为存档-->
            <RollingFile name="RollingFile1" fileName="logs/log1.log"
                         filePattern="logs/$${date:yyyy-MM}/log-%d{MM-dd-yyyy}-%i.log">
                <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
                <SizeBasedTriggeringPolicy size="100MB"/>
            </RollingFile>

    3.1.5 其它appender

    我们已经介绍了一些经经常使用到的Appenders,还有非常多其他Appender。

    它们加入了新功能或者在其他的一些Appender基础上实现了新功能。比如,Log4j中的RollingFileAppender扩展了FileAppender。它能够在满足特定条件时自己主动创建新的日志文件。SMTPAppender会将日志内容以邮件的形式发送出去;FailoverAppender会在处理日志的过程中,假设一个或者多个Appender失败,自己主动切换到其他Appender上。

    假设想了解其它能够參考:https://logging.apache.org/log4j/2.0/manual/appenders.html

    3.2 Layouts

    Layouts将日志记录的内容从一种数据形式转换成第二种。日志框架为纯文本、HTML、syslog、XML、JSON、序列化以及其他日志提供了Layouts。

    这里贴一篇文章简介下我们经常使用的PatternLayout :http://wiki.jikexueyuan.com/project/log4j/log4j-patternlayout.html

    其它的layouts配置能够參考:https://logging.apache.org/log4j/2.0/manual/layouts.html

    <PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>


    3.3 Loggers

    Logger负责捕捉事件并将其发送给合适的Appender。Logger之间是有继承关系的。

    总是存在一个rootLogger,即使没有显示配置也是存在的,而且默认输出级别为DEBUG。其他的logger都继承自这个rootLogger。
    Log4J中的继承关系是通过名称能够看出来,如"A"、"A.B"、"A.B.C",A.B继承A。A.B.C继承A.B,比較类似于包名。

    <loggers>
            <logger name="com.sankuai" level="info" includeLocation="true" additivity="true">
                <appender-ref ref="RollingFile2"/>
                <appender-ref ref="RollingFile1"/>
            </logger>
    		<logger name="com.sankuai.meituan" level="error" includeLocation="true" additivity="true">
                <appender-ref ref="RollingFile2"/>
                <appender-ref ref="RollingFile1"/>
            </logger>
            <!--建立一个默认的root的logger-->
            <root level="error">
                <appender-ref ref="Console"/>
                <appender-ref ref="RollingFile1"/>
            </root>
        </loggers>



    additivity是 子Logger 是否继承 父Logger 的 输出源(appender) 的标志位。详细说,默认情况下 子Logger 会继承 父Logger 的appender,也就是说 子Logger 会在 父Logger 的appender里输出。若是additivity设为false,则 子Logger 仅仅会在自己的appender里输出。而不会在 父Logger 的appender里输出。 

    3.4 日志级别

    DEBUG , INFO ,WARN ,ERROR四种,分别相应Logger类的四种方法
    debug(Object message ) ;
    info(Object message ) ;
    warn(Object message ) ;
    error(Object message ) ;
    假设设置级别为INFO。则优先级大于等于INFO级别(如:INFO、WARN、ERROR)的日志信息将能够被输出,
    小于该级别的如:DEBUG将不会被输出

    4、Log4j2 AsyncLogger与AsyncAppender

    先上图



    第一张图能够看出Log4j2的asyncLogger的性能较使用asyncAppender和sync模式有很大的提升。特别是线程越多的时候。

    第二张图是将log4j2的异步日志机制和其它日志系统进行对照,log4j2的asyncLogger 性能也是非常有优势。

    这里主要涉及了两个概念AsyncLogger和AysncAppender。是支持异步的Logger和Appender,以下分别简要介绍下这两个概念。

    4.1 AsyncAppender

    AsyncAppender持有其它的配置了aysnc的appender引用列表(appender须要通过配置注冊成异步的)。当其它的logger须要打日志的时候(logEvent事件),asyncAppender会接收logEvent,缓存到queue中,然后用单独的线程完毕从queue中取logEvent打印到目的appender,这个逻辑比較简单,看下源代码就能明确这个流程。ps. AsyncAppender是Log4j 和Log4j2 都有的,不是新东西,但从上面的性能对照上还是有一点点差异的。基本的原因是:(引用官方说法)Asynchronous Appenders already existed in Log4j 1.x, but have been enhanced to flush to disk at the end of a batch (when the queue is empty).

    关于AsyncAppender能提高性能的场景。能够看下这个篇文章。

     http://littcai.iteye.com/blog/316605

    怎样配置一个AsyncAppender:

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="warn" name="MyApp" packages="">
      <Appenders>
        <File name="MyFile" fileName="logs/app.log">
          <PatternLayout>
            <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
          </PatternLayout>
        </File>
        <Async name="Async">
          <AppenderRef ref="MyFile"/>
        </Async>
      </Appenders>
      <Loggers>
        <Root level="error">
          <AppenderRef ref="Async"/>
        </Root>
      </Loggers>
    </Configuration>


    @Plugin(name = "Async", category = "Core", elementType = "appender", printObject = true)
    public final class AsyncAppender extends AbstractAppender {
        private static final long serialVersionUID = 1L;
        private static final int DEFAULT_QUEUE_SIZE = 128;
        private static final String SHUTDOWN = "Shutdown";
        private static final AtomicLong THREAD_SEQUENCE = new AtomicLong(1);
        private static ThreadLocal<Boolean> isAppenderThread = new ThreadLocal<>();
        private final BlockingQueue<Serializable> queue;
        private final int queueSize;
        private final boolean blocking;
        private final long shutdownTimeout;
        private final Configuration config;
        private final AppenderRef[] appenderRefs;
        private final String errorRef;
        private final boolean includeLocation;
        private AppenderControl errorAppender;
        private AsyncThread thread;
        private AsyncAppender(final String name, final Filter filter, final AppenderRef[] appenderRefs,
                final String errorRef, final int queueSize, final boolean blocking, final boolean ignoreExceptions,
                final long shutdownTimeout, final Configuration config, final boolean includeLocation) {
            super(name, filter, null, ignoreExceptions);
            this.queue = new ArrayBlockingQueue<>(queueSize);
            this.queueSize = queueSize;
            this.blocking = blocking;
            this.shutdownTimeout = shutdownTimeout;
            this.config = config;
            this.appenderRefs = appenderRefs;
            this.errorRef = errorRef;
            this.includeLocation = includeLocation;
        }
        @Override
        public void start() {
            final Map<String, Appender> map = config.getAppenders();
            final List<AppenderControl> appenders = new ArrayList<>();
            for (final AppenderRef appenderRef : appenderRefs) {
                final Appender appender = map.get(appenderRef.getRef());
                if (appender != null) {
                    appenders.add(new AppenderControl(appender, appenderRef.getLevel(), appenderRef.getFilter()));
                } else {
                    LOGGER.error("No appender named {} was configured", appenderRef);
                }
            }
            if (errorRef != null) {
                final Appender appender = map.get(errorRef);
                if (appender != null) {
                    errorAppender = new AppenderControl(appender, null, null);
                } else {
                    LOGGER.error("Unable to set up error Appender. No appender named {} was configured", errorRef);
                }
            }
            if (appenders.size() > 0) {
                thread = new AsyncThread(appenders, queue);
                thread.setName("AsyncAppender-" + getName());
            } else if (errorRef == null) {
                throw new ConfigurationException("No appenders are available for AsyncAppender " + getName());
            }
            thread.start();
            super.start();
        }
        @Override
        public void stop() {
            super.stop();
            LOGGER.trace("AsyncAppender stopping. Queue still has {} events.", queue.size());
            thread.shutdown();
            try {
                thread.join(shutdownTimeout);
            } catch (final InterruptedException ex) {
                LOGGER.warn("Interrupted while stopping AsyncAppender {}", getName());
            }
            LOGGER.trace("AsyncAppender stopped. Queue has {} events.", queue.size());
        }
        /**
         * Actual writing occurs here.
         * 
         * @param logEvent The LogEvent.
         */
        @Override
        public void append(LogEvent logEvent) {
            if (!isStarted()) {
                throw new IllegalStateException("AsyncAppender " + getName() + " is not active");
            }
            if (!(logEvent instanceof Log4jLogEvent)) {
                if (!(logEvent instanceof RingBufferLogEvent)) {
                    return; // only know how to Serialize Log4jLogEvents and RingBufferLogEvents
                }
                logEvent = ((RingBufferLogEvent) logEvent).createMemento();
            }
            logEvent.getMessage().getFormattedMessage(); // LOG4J2-763: ask message to freeze parameters
            final Log4jLogEvent coreEvent = (Log4jLogEvent) logEvent;
            boolean appendSuccessful = false;
            if (blocking) {
                if (isAppenderThread.get() == Boolean.TRUE && queue.remainingCapacity() == 0) {
                    // LOG4J2-485: avoid deadlock that would result from trying
                    // to add to a full queue from appender thread
                    coreEvent.setEndOfBatch(false); // queue is definitely not empty!
                    appendSuccessful = thread.callAppenders(coreEvent);
                } else {
                    final Serializable serialized = Log4jLogEvent.serialize(coreEvent, includeLocation);
                    try {
                        // wait for free slots in the queue
                        queue.put(serialized);
                        appendSuccessful = true;
                    } catch (final InterruptedException e) {
                        // LOG4J2-1049: Some applications use Thread.interrupt() to send
                        // messages between application threads. This does not necessarily
                        // mean that the queue is full. To prevent dropping a log message,
                        // quickly try to offer the event to the queue again.
                        // (Yes, this means there is a possibility the same event is logged twice.)
                        //
                        // Finally, catching the InterruptedException means the
                        // interrupted flag has been cleared on the current thread.
                        // This may interfere with the application's expectation of
                        // being interrupted, so when we are done, we set the interrupted
                        // flag again.
                        appendSuccessful = queue.offer(serialized);
                        if (!appendSuccessful) {
                            LOGGER.warn("Interrupted while waiting for a free slot in the AsyncAppender LogEvent-queue {}",
                                    getName());
                        }
                        // set the interrupted flag again.
                        Thread.currentThread().interrupt();
                    }
                }
            } else {
                appendSuccessful = queue.offer(Log4jLogEvent.serialize(coreEvent, includeLocation));
                if (!appendSuccessful) {
                    error("Appender " + getName() + " is unable to write primary appenders. queue is full");
                }
            }
            if (!appendSuccessful && errorAppender != null) {
                errorAppender.callAppender(coreEvent);
            }
        }
        /**
         * Create an AsyncAppender.
         * 
         * @param appenderRefs The Appenders to reference.
         * @param errorRef An optional Appender to write to if the queue is full or other errors occur.
         * @param blocking True if the Appender should wait when the queue is full. The default is true.
         * @param shutdownTimeout How many milliseconds the Appender should wait to flush outstanding log events
         *                        in the queue on shutdown. The default is zero which means to wait forever.
         * @param size The size of the event queue. The default is 128.
         * @param name The name of the Appender.
         * @param includeLocation whether to include location information. The default is false.
         * @param filter The Filter or null.
         * @param config The Configuration.
         * @param ignoreExceptions If {@code "true"} (default) exceptions encountered when appending events are logged;
         *            otherwise they are propagated to the caller.
         * @return The AsyncAppender.
         */
        @PluginFactory
        public static AsyncAppender createAppender(@PluginElement("AppenderRef") final AppenderRef[] appenderRefs,
                @PluginAttribute("errorRef") @PluginAliases("error-ref") final String errorRef,
                @PluginAttribute(value = "blocking", defaultBoolean = true) final boolean blocking,
                @PluginAttribute(value = "shutdownTimeout", defaultLong = 0L) final long shutdownTimeout,
                @PluginAttribute(value = "bufferSize", defaultInt = DEFAULT_QUEUE_SIZE) final int size,
                @PluginAttribute("name") final String name,
                @PluginAttribute(value = "includeLocation", defaultBoolean = false) final boolean includeLocation,
                @PluginElement("Filter") final Filter filter, @PluginConfiguration final Configuration config,
                @PluginAttribute(value = "ignoreExceptions", defaultBoolean = true) final boolean ignoreExceptions) {
            if (name == null) {
                LOGGER.error("No name provided for AsyncAppender");
                return null;
            }
            if (appenderRefs == null) {
                LOGGER.error("No appender references provided to AsyncAppender {}", name);
            }
            return new AsyncAppender(name, filter, appenderRefs, errorRef, size, blocking, ignoreExceptions,
                    shutdownTimeout, config, includeLocation);
        }
        /**
         * Thread that calls the Appenders.
         */
        private class AsyncThread extends Thread {
            private volatile boolean shutdown = false;
            private final List<AppenderControl> appenders;
            private final BlockingQueue<Serializable> queue;
            public AsyncThread(final List<AppenderControl> appenders, final BlockingQueue<Serializable> queue) {
                this.appenders = appenders;
                this.queue = queue;
                setDaemon(true);
                setName("AsyncAppenderThread" + THREAD_SEQUENCE.getAndIncrement());
            }
            @Override
            public void run() {
                isAppenderThread.set(Boolean.TRUE); // LOG4J2-485
                while (!shutdown) {
                    Serializable s;
                    try {
                        s = queue.take();
                        if (s != null && s instanceof String && SHUTDOWN.equals(s.toString())) {
                            shutdown = true;
                            continue;
                        }
                    } catch (final InterruptedException ex) {
                        break; // LOG4J2-830
                    }
                    final Log4jLogEvent event = Log4jLogEvent.deserialize(s);
                    event.setEndOfBatch(queue.isEmpty());
                    final boolean success = callAppenders(event);
                    if (!success && errorAppender != null) {
                        try {
                            errorAppender.callAppender(event);
                        } catch (final Exception ex) {
                            // Silently accept the error.
                        }
                    }
                }
                // Process any remaining items in the queue.
                LOGGER.trace("AsyncAppender.AsyncThread shutting down. Processing remaining {} queue events.",
                        queue.size());
                int count = 0;
                int ignored = 0;
                while (!queue.isEmpty()) {
                    try {
                        final Serializable s = queue.take();
                        if (Log4jLogEvent.canDeserialize(s)) {
                            final Log4jLogEvent event = Log4jLogEvent.deserialize(s);
                            event.setEndOfBatch(queue.isEmpty());
                            callAppenders(event);
                            count++;
                        } else {
                            ignored++;
                            LOGGER.trace("Ignoring event of class {}", s.getClass().getName());
                        }
                    } catch (final InterruptedException ex) {
                        // May have been interrupted to shut down.
                        // Here we ignore interrupts and try to process all remaining events.
                    }
                }
                LOGGER.trace("AsyncAppender.AsyncThread stopped. Queue has {} events remaining. "
                        + "Processed {} and ignored {} events since shutdown started.", queue.size(), count, ignored);
            }
            /**
             * Calls {@link AppenderControl#callAppender(LogEvent) callAppender} on all registered {@code AppenderControl}
             * objects, and returns {@code true} if at least one appender call was successful, {@code false} otherwise. Any
             * exceptions are silently ignored.
             *
             * @param event the event to forward to the registered appenders
             * @return {@code true} if at least one appender call succeeded, {@code false} otherwise
             */
            boolean callAppenders(final Log4jLogEvent event) {
                boolean success = false;
                for (final AppenderControl control : appenders) {
                    try {
                        control.callAppender(event);
                        success = true;
                    } catch (final Exception ex) {
                        // If no appender is successful the error appender will get it.
                    }
                }
                return success;
            }
            public void shutdown() {
                shutdown = true;
                if (queue.isEmpty()) {
                    queue.offer(SHUTDOWN);
                }
            }
        }
        /**
         * Returns the names of the appenders that this asyncAppender delegates to as an array of Strings.
         * 
         * @return the names of the sink appenders
         */
        public String[] getAppenderRefStrings() {
            final String[] result = new String[appenderRefs.length];
            for (int i = 0; i < result.length; i++) {
                result[i] = appenderRefs[i].getRef();
            }
            return result;
        }
        /**
         * Returns {@code true} if this AsyncAppender will take a snapshot of the stack with every log event to determine
         * the class and method where the logging call was made.
         * 
         * @return {@code true} if location is included with every event, {@code false} otherwise
         */
        public boolean isIncludeLocation() {
            return includeLocation;
        }
        /**
         * Returns {@code true} if this AsyncAppender will block when the queue is full, or {@code false} if events are
         * dropped when the queue is full.
         * 
         * @return whether this AsyncAppender will block or drop events when the queue is full.
         */
        public boolean isBlocking() {
            return blocking;
        }
        /**
         * Returns the name of the appender that any errors are logged to or {@code null}.
         * 
         * @return the name of the appender that any errors are logged to or {@code null}
         */
        public String getErrorRef() {
            return errorRef;
        }
        public int getQueueCapacity() {
            return queueSize;
        }
        public int getQueueRemainingCapacity() {
            return queue.remainingCapacity();
        }
    }


    AsyncLogger是Log4j2引入的新特性,业务代码调用Logger.log的时候直接返回。而不须要等到appender输出到日志目的地后才返回。

    Log4j2的Asynclogger是通过LMAX Disruptor取代queue实现的异步(无锁的并发框架,http://ifeve.com/disruptor/Disruptor简单介绍),达到更高的并发和lower latency。

    4.2 AsyncLogger


    1,Disruptor使用了一个RingBuffer替代队列,用生产者消费者指针替代锁。


    2,生产者消费者指针使用CPU支持的整数自增。无需加锁而且速度非常快。Java的实如今Unsafe package中。


    虽然AsyncLogger 可以大幅度的提高性能。可是也会带来一些问题。以下是翻译官方的文档的Trade-offs:

    Benefits

    • Higher throughput,达到相对于sync logger的6-68倍的吞吐量
    • Lower logging latency,latency是调用Logger.log直到return的时间。asyncLogger的latency比syncLogger以及基于queue的aysncAppender都要低,不仅平均latency低,并且99%、95%latency 也都低于后两者
    • 减少极端大的日志量时候的延迟尖峰

    Drawbacks

    • Error handling, 假设在打印日志的时候出现错误,使用asyncLogger。业务是不知道异常的(能够通过配置ExceptionHandler处理异常)。假设打印日志是业务逻辑的一部分,不建议使用asyncLogger
    • 打印一些可变的内容的时候。使用asyncLogger 会出现故障。大部分时间,不须要操心这点。Log4j确保了类似于 logger.debug("My object is {}", myObject),使用myObject在打印日志的时刻的版本号打印(Log4j 全部打印都日志都是封装到Message的实现类里,存储在 final String里),无论之后是否改变。可是log4j也支持一些了可变的Message,如 MapMessage and StructuredDataMessage 。这些假设在打印日志时候改变,就有问题了

    全局配置异步Logger

    配置全部Logger都为AsyncLogger,仅仅须要添加disruptor包,然后配置一个system property,-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector。Log4j的配置文件不须要改动。

    混合使用同步和异步Logger

    单独配置某个logger为async的,通过<asyncRoot>或者<asyncLogger>

    <Configuration status="WARN">
      <Appenders>
        <!-- Async Loggers will auto-flush in batches, so switch off immediateFlush. -->
        <RandomAccessFile name="RandomAccessFile" fileName="asyncWithLocation.log"
                  immediateFlush="false" append="false">
          <PatternLayout>
            <Pattern>%d %p %class{1.} [%t] %location %m %ex%n</Pattern>
          </PatternLayout>
        </RandomAccessFile>
      </Appenders>
      <Loggers>
        <!-- pattern layout actually uses location, so we need to include it -->
        <AsyncLogger name="com.foo.Bar" level="trace" includeLocation="true">
          <AppenderRef ref="RandomAccessFile"/>
        </AsyncLogger>
        <Root level="info" includeLocation="true">
          <AppenderRef ref="RandomAccessFile"/>
        </Root>
      </Loggers>
    </Configuration>


    ps. location的问题

    当layouts配置了输出%C or $class, %F or %file, %l or %location, %L or %line, %M or %method,或者HTML locationInfo,  log4j会获取location的一个快照,而这对于sync 和async的logger都是一个耗时的操作(官方文档上说syncLogger会慢1.3~5倍。async会慢4-20倍)。所以默认都是不会输出location信息,除非Logger配置了includeLocation="true"(官方文档这么说的,可是我測试的是默认是输出的,无论了。反正当日志出现慢的时候,能够考虑通过配置includeLocation控制是否输出location信息)。




    =====================================以下为作者自己探究================

    由于上面的架构图,只是描述了关系。为了实际使用,我们不可能同时使用以上系统中的多个,只能选一个。我在这里选择了log4j2作为springmvc项目的日志记录系统。

    我们知道log4j与log4j2实际上并不是连贯的一套日志系统,因为log4j2不兼容log4j.因此,当我们使用log4j2的时候,就必须避开log4j版本的jar包。

    log4j2项目是apache项目,具体官网下载地址:

    https://www.apache.org/dyn/closer.lua/logging/log4j/2.10.0/apache-log4j-2.10.0-bin.zip

    这里下载之后,解压发现里面有居多jar包,而不仅仅是一个。


    具体有这一些。

    当使用springmvc项目的时候,是需要配置filter,servlet的。所以,

    <!-- log4j配置,文件路径,因为是跟随项目启动 -->
     <context-param>
      <param-name>log4jConfigLocation</param-name>
      <param-value>/WEB-INF/log4j2.xml</param-value>
     </context-param>
     <!-- log4j2-begin -->
     <listener>
      <listener-class>org.apache.logging.log4j.web.Log4jServletContextListener</listener-class>
     </listener>
     <filter>
      <filter-name>log4jServletFilter</filter-name>
      <filter-class>org.apache.logging.log4j.web.Log4jServletFilter</filter-class>
     </filter>
     <filter-mapping>
      <filter-name>log4jServletFilter</filter-name>
      <url-pattern>/*</url-pattern>
      <!-- <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher>
       <dispatcher>INCLUDE</dispatcher> <dispatcher>ERROR</dispatcher> -->
     </filter-mapping>
     <!-- log4j2-end -->


    在web.xml中需要加上这段代码。


    pom文件记录如下:

    <properties>
      <slf4j.version>1.7.25</slf4j.version>
      <log4j.version>2.8.2</log4j.version>
      <spring.version>4.3.14.RELEASE</spring.version>
     </properties>

    <!-- 日志开始 -->
      <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
       <version>${slf4j.version}</version>
      </dependency>
      <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-slf4j-impl</artifactId>
       <version>${log4j.version}</version>
      </dependency>
      <!--兼容log4j -->
      <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-1.2-api</artifactId>
       <version>2.0</version>
      </dependency>
      <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
       <version>${log4j.version}</version>
      </dependency>
      <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-core</artifactId>
       <version>${log4j.version}</version>
      </dependency>
      <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-web</artifactId>
       <version>${log4j.version}</version>
      </dependency>

      <!--log4j2 异步appender须要 -->
      <dependency>
       <groupId>com.lmax</groupId>
       <artifactId>disruptor</artifactId>
       <version>3.2.0</version>
      </dependency>

      <!--日志结束 -->


    注意:这里不仅需要log4j-core,还需要log4j-web.jar。这都是log4j的部分。所以,可以把图上的jar包根据情况进行添加。

    日志配置文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="off" monitorInterval="1800">
     <properties>
      <property name="LOG_HOME">d://logs/tomcat-8081</property>
      <property name="FILE_NAME">springRedisCloudTest</property>
     </properties>
     <Appenders>
      <Console name="Console" target="SYSTEM_OUT">
       <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
      </Console>
      <RollingRandomAccessFile name="running-log"
       fileName="${LOG_HOME}/${FILE_NAME}.log"
       filePattern="${LOG_HOME}/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log.gz">
       <PatternLayout
        pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %level [%thread][%file:%line] - %msg%n" />
       <Policies>
        <TimeBasedTriggeringPolicy />
        <SizeBasedTriggeringPolicy size="10 MB" />
       </Policies>
       <DefaultRolloverStrategy max="20" />
      </RollingRandomAccessFile>
     </Appenders>
     <Loggers>
      <Logger name="springRedisCloudTest" level="info"
       additivity="true">
       <AppenderRef ref="running-log" />
      </Logger>
      <Root level="info">
       <AppenderRef ref="Console" />
      </Root>
     </Loggers>
    </Configuration>


    使用:

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

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

    logger.info("PRINT A MARK IN THE METHOD");







    
    展开全文
  • Java帝国在诞生之初就提供了集合、线程、IO、网络等常用功能,从C和C++领地那里吸引了大量程序员过来加盟,但是却有意无意地忽略了一个重要的功能: 输出日志。 对于这一点,IO大臣其实非常清楚, 日志是...

    1前言

    Java帝国在诞生之初就提供了集合、线程、IO、网络等常用功能,从C和C++领地那里吸引了大量程序员过来加盟,但是却有意无意地忽略了一个重要的功能: 输出日志。


    对于这一点,IO大臣其实非常清楚, 日志是个很重要的东西, 因为程序运行起来以后, 基本上就是一个黑盒子,如果程序的行为和预料的不一致,那就是出现Bug了,如何去定位这个Bug 呢?   


    臣民们能用的工具有两个,第一个就是单步调试,一步步地跟踪,查看代码中变量的值, 这种办法费时费力, 并且只能在程序员的机器上才能用。 


    第二种就是在特定的地方打印日志, 通过日志的输出,帮助快速定位。尤其是当代码在生产环境上跑起来以后, 日志信息更是必不可少,要不然出了状况两眼一抹黑,上哪儿找问题去? 总不能让臣民们把自己变成一个线程进入系统来执行吧?


    但是IO大臣也有自己的小算盘: 日志嘛, 用我的System.out.println(.....) 不就可以了?!   我还提供了System.err.println不是?  


    在IO大臣的阻挠下, 从帝国的第一代国王到第三代国王, 都没有在JDK中提供日志相关的工具包, 臣民们只好忍受着去使用System.out.println去输出日志,把所有的信息都输出到控制台, 让那里变成一堆垃圾。


    2张家村

     张家村的电子商务系统也不能幸免,自然也遇到了日志的问题。经验丰富的老村长已经烦透了System.out.println所输出的大量难于理解的无用信息,看着村民民整天苦逼地和这些System.out做斗争,他找来了小张,命令他设计一个通用的处理日志的系统。


    小张在消息队列和JMS的设计上花了不少功夫, 积累了丰富的经验,从那以后一直都是实现业务代码,一直都是CRUD, 张二妮整天笑话自己是HTML填空人员,这一回一定要让她看看自己的设计功力!


    (传送门:《Java帝国之消息队列》,《Java帝国之JMS的诞生》)


    老村长给小张下达的需求是这样的:


    1.  日志消息除了能打印到控制台, 还可以输出到文件,甚至可以通过邮件发送出去(例如生成环境出错的消息)


    2. 日志内容应该可以做格式化, 例如变成纯文本,XML, HTML格式等等


    3. 对于不同的Java class,不同的 package , 还有不同级别的日志,应该可以灵活地输出到不同的文件中。

    例如对于com.foo 这个package,所有的日志都输出到 foo.log 文件中

    对于com.bar 这个package ,所有文件都输出到bar. log文件中

    对于所有的ERROR级别的日志,都输出到  errors.log文件中


    4. 能对日志进行分级, 有些日志纯属debug , 在本机或者测试环境使用, 方便程序员的调试, 生产环境完全不需要。有些日志是描述错误(error)的, 在生产环境下出错的话必须要记录下来,帮助后续的分析。


    小张仔细看了看,拍着胸脯对老村长说:“没问题, 明天一定让您老看到结果。”


    3小张的设计

    老村长走了以后,小张开始分析需求, 祭出“面向对象设计大法”,试图从村长的需求中抽象出一点概念。


    首先要记录日志,肯定需要一个类来表达日志的概念,这个类至少应该有两个属性,一个是时间戳,一个是消息本身,把它叫做LoggingEvent吧,记录日志就像记录一个事件嘛。


    其次是日志可以输出到不同的地方,控制台、文件、邮件等等, 这个可以抽象一下,不就是写到不同的目的地吗? 可以叫做LogDestination? 


    嗯, 还是简单一点,叫做Appender吧, 暗含了可以不断追加日志的意思。


    至于第二条的日志内容可以格式化,完全可以比葫芦画瓢, 定义一个Formatter接口去格式化消息。


    对了, Appender 应该引用Formatter ,这样以来就可以对LoggingEvent记录格式化以后再发送。 


    第三条需求把小张给难住了,不同的class, package 输出的目的地不同?  “目的地”这个概念是由Appender来表达的, 难道让不同的class, package 和Appender关联? 不不, 不能这样 ! 


    还需要一个新的概念 , 这个概念是什么? 


    从用户角度想一下, 村民们要想获取日志,必须得先获取个什么东西,这个东西是不是可以称为Logger啊?  灵感的火花就闪了那么一下就被小张抓住了: 获取Logger的时候要传入类名或者包名! 


    这样一来,不同的class, package就区分开了, 然后让Logger 和Appender关联,灵活地设置日志的目的地, 并且一个Logger可以拥有多个Appender,同一条日志消息可以输出到多个地方, 完美!


    小张迅速地画出了核心类的类图




    还算漂亮,小张陶醉着自我欣赏了一下。 


    再接再厉, 把第四条需求也设计一下,日志要分级,这个简单, 定义一个Priority的类,里边定义5个常量DEBUG, INFO, WARN, ERROR, FATAL, 表示5个不同的级别就OK了。当然这我5个级别有高低之分, DEBUG级别最低, FATAL级别最高。


    还可以给Logger增加一些辅助编程的方法,如Logger.debug(....) , Logger.info(...)  , Logger.warn(...) 等等, 这样村民们将来就可以轻松地输出各种级别的日志了。


    等一下, 老村长还说过“对于所有的ERROR级别的日志,都输出到  errors.log文件中” 类似这样的需求, 好像给忽略了。


    这也好办嘛, 只要在Appender上增加一个属性,就叫做Priority, 如果用户要输出的日志是DEBUG级别, 但是有个FileAppender的Priority是 ERROR级别,那这个日志就不用在这个FileAppender中输出了 ,因为ERROR级别比DEBUG级别高嘛。


    同理, 在Logger类上也可以增加一个Priority的属性,用户可以去设置, 如果一个Logger的Priority是ERROR, 而用户调用了这个Logger的debug方法, 那这个debug 的消息也不会输出。


    小张全心全意地投入到设计当中,一看时间, 都快半夜了, 赶紧休息, 明天向村长汇报去。 


    4正交性

    第二天, 小张给老村长展示了自己设计的LoggerEvent, Logger , Appender, Formatter, Priority 等类和接口, 老村长捻着胡子满意地点点头:“不错不错,与上一次相比有巨大的进步。你知不知道我在需求中其实给了你引导?”


    “引导? 什么引导? ”


    “就是让你朝着正交的方向去努力啊”


    “正交? ”


    ‘“如果你把Logger, Appender, Formatter看成坐标系中的X轴,Y轴,Z轴, 你看看,这三者是不是可以独立变化而不互相影响啊?”


    “我赛,果然如此,我可以任意扩展Appender接口而影响不到Logger和Formatter, 无论有多少个Logger 都影响不了Appender和Formatter , 这就是正交了?”


    “是啊,当你从系统中提取出正交的概念的时候,那就威力无比了,因为变化被封装在了一个维度上,你可以把这些概念任意组合,而不会变成意大利面条似的代码。 ”


    听到村长做了理论的升华, 小张兴奋得直搓手。 


    “好吧,你把这个设计实现了吧,对了,你打算叫什么名字? ”  村长问道


    “我打算把他叫做Log4j , 意思是Log for Java”


    “不错,就这么定了吧”


    5Log4j

    小张又花了两个月的时间把Log4j 开发了出来, 由于Log4j有着良好的设计,优异的性能, 不仅仅是张家村的人在用, Java帝国的很多村镇、部落都爱上了它。


    后来张家村把Log4j 在Apache部落开源了, 这下子吸引了无数的人无偿帮助测试它,扩展它,改进它, 很快就成了帝国最流行的日志工具。


    张家村建议帝国把Log4j 纳入到JDK 中, 帝国那效率低下的官僚机构竟然拒绝了。  消息传到了IO大臣的耳朵里,他不由的扼腕叹息: 唉,失去了一次极好的招安机会啊。 现在唯一的办法就是赶紧上奏皇上,在官方也提供一套,争取让臣民们使用官方版本。


    到了第四代国王(JDK1.4),臣民们终于看到了帝国提供的java.util.logging包,也是用来记录日志的,并且其中的核心概念Logger, Formatter, Handler 和 Log4j非常相似,只是为时已晚, Log4j早已深入人心了, 不可撼动了。  


    6尾声

    Log4j 在Apache开源以后, 小张也逐渐地有点落寞,他闲不住又写了一个工具,叫做logback, 有了之前的经验,这logback 比log4j 还要快。 


    如今的日志世界有了很多的选择 ,除了java.util.logging, log4j 之外,还有logback,tinylog 等其他工具。


    小张想了想, 这么多日志工具,用户如果想切换了怎么办?不想用log4j了,能换到logback吗? 


    我还是提供一个抽象层吧, 用户用这个抽象层的API来写日志, 底层具体用什么日志工具不用关心,这样就可以移植了。 


    小张把这抽象层就叫做Simple Logging Facade for Java,简称SLF4J。  


    对于Log4j , JDK logging, tinylog 等工具, 需要一个适配层, 把SLF4J 的API转化成具体工具的调用接口。 


    由于Logback这个工具也是出自小张之手, 直接实现了SLF4J的API,所以连适配层都不需要了, 用起来速度飞快,效率最高,SLFJ4+Logback 成为了很多人的最爱, 大有超越Apache Common Logging + Log4j 之势。 


    后记: 本文主要想讲一下日志工具的历史和现状, 尤其是Log4j核心的设计理念。


    文中的小张其实就是Ceki Gülcü,他开发了Log4j , logback,以及slfj4, 为Java的日志事业做出了卓越的贡献。

    转自老刘的博客

    展开全文
  • 日志系统架构

    2020-01-14 11:49:23
    日志系统方案参考; http:自定义的采集数据,存储到es,通过kibana可视化定制报表数据 日志文件:filebeat采集应用服务器日志,根据关键字预警,通过kibana的Sentinl或者elastalert告警 Agent:通过探针跟踪链和原...

    日志系统方案参考;

    • http:自定义的采集数据,存储到es,通过kibana可视化定制报表数据
    • 日志文件:filebeat采集应用服务器日志,根据关键字预警,通过kibanaSentinl或者elastalert告警
    • Agent:通过探针跟踪链和原数据的监控(慢sql、慢方法)
    • Webuidashboard定制化日志报表;skywalkingUi主要是跟踪链和原数据,kibana除展示es的日志数,也是es的可视化工具

    报表的一些重要维度,方便kibana可以画一些报表

    时间
    系统(crmcrs,opendapi
    组件(线程池、sql、慢方法)
    告警级别(infoerror)
    告警类别(systemapplication,business
    标签(重要性、版本号、URLporttraceid
    主机(ipcenteros、)
    原数据

    展开全文
  • JAVA日志系统

    千次阅读 2018-08-15 12:53:24
    JAVA日志系统的演变史 我们先看一个故事。项目经理A带着一帮兄弟开发了一套复杂的企业ERP系统,这个系统一连开发了好几年,开发人员也换了好几拨。 阶段一:最开始的时候,项目经理A安排小B在系统中添加日志功能,...
  • Java 日志系统

    2018-07-24 16:26:44
    java的日志系统繁杂,今天趁着解决日志系统冲突的过程,顺带学习一下java的日志系统并做个记录 日志演化历史 最开始出现的是log4j,也是应用最广泛的日志系统,成为了目前java日志系统事实上的标准,一切都是美好...
  • 本文将从海量日志系统在优化、部署、监控方向如何更适应业务的需求入手,重点从多种日志系统的架构设计对比;后续调优过程:横向扩展与纵向扩展,分集群,数据分治,重写数据链路等实际现象与问题展开。日志系统架构...
  • 日志-syslog系统日志

    万次阅读 2017-02-17 20:47:10
    syslog是Linux系统默认的日志守护进程。默认的syslog配置文件是/etc/sysctl.conf文件 通常,syslog 接受来自系统的各种功能的信息,每个信息都包括重要级。/etc/syslog.conf 文件通知 syslogd 如何根据设备和信息...
  • ELK日志系统

    千次阅读 2019-06-13 09:03:03
    作为系列文章的第四篇,本文将重点探讨数据采集层中的ELK日志系统。日志,指的是后台服务中产生的log信息,通常会输入到不同的文件中,比如Django服务下,一般会有nginx日志和uWSGI日志。这些日志分散地存储在不同的...
  • 系统日志

    千次阅读 2018-04-14 09:04:43
    【Linux日志系统日志及分析Linux系统拥有非常灵活和强大的日志功能,可以保存几乎所有的操作记录,并可以从中检索出我们需要的信息。大部分Linux发行版默认的日志守护进程为 syslog,位于 /etc/syslog 或 /etc/...
  • 百亿级日志系统架构设计及优化

    千次阅读 2018-10-07 22:43:50
    本文将从海量日志系统在优化、部署、监控方向如何更适应业务的需求入手,重点从多种日志系统的架构设计对比;后续调优过程:横向扩展与纵向扩展,分集群,数据分治,重写数据链路等实际现象与问题展开。 日志系统...
  • SuSE Linux常见日志清单

    万次阅读 2014-11-24 19:20:05
    介绍SuSE Linux操作系统常见的日志文件和说明。 SuSE Linux操作系统常见的日志文件列表如表1所示。 表1 SuSE Linux常见日志清单 日志  说明   /var/log/boot.msg  系统启动时显示的屏幕信息。当无法登录...
  • 教你如何查看win10系统日志

    万次阅读 2016-10-17 23:04:32
    Windows日志包含了应用程序、安全和系统日志,当电脑因为误操作出现故障时,用户可以通过查看系统日志来查看系统错误,进而找到解决方案。如何查看win10系统日志呢?  1、右击开始菜单,选择“事件查看器...
  • 事件ID:12事件ID 13:事件ID 41:事件ID 6008:事件ID 1074:关闭电源(关机): 重启:
  • 【Monogdb】MongoDB的日志系统

    万次阅读 2014-12-01 23:38:22
    记得前几天有个小伙伴要查看mongodb的日志,从而排查问题,可能总找不到日志放在何处,今天就系统说一下mongodb的日志系统。mongodb中主要有四种日志。分别是系统日志、Journal日志、oplog主从日志、慢查询日志等。...
  • 查看windows系统的开关机日志

    万次阅读 2018-05-30 18:19:00
    Win7系统查看开关机日志的操作方法(图文) 今天本教程教大家查看win7系统开关机日志,有时候离开电脑后,不知道别人有没有动过自己电脑,我们可以通过查看Win7系统查看开关机日志来确定电脑是否被人动过。那么Win...
  • 系统启动后的信息和错误日志,是Red Hat Linux中最常用的日志之一 /var/log/secure 与安全相关的日志信息 /var/log/maillog 与邮件相关的日志信息 /var/log/cron 与定时任务相关的日志信息 /var/log/spooler ...
  • AIX系统日志查看

    万次阅读 2011-08-01 14:47:03
    在进行AIX 的日常维护时,需关注的日志文件有: 文件 描述 建议 core 和 snapcore 由应用产生的 dump 文件, 可用于诊断错误 可删除 nohup.out nohup 命令的输出结果 可删除 .
  • 日志管理方法及系统

    万次阅读 2016-01-16 12:34:48
    本发明涉及应用系统日志管理技术领域,提供了一种日志管理方法和系统,所述方法包括如下步骤:S1:初始化系统业务功能列表和业务功能方法列表;S2:将业务操作中的具体操作信息与系统日志表和历史数据日志表直接相...
  • 系统日志管理

    万次阅读 2017-02-22 10:19:47
    我们可以使用日志系统所记录的信息为系统进行排错,优化系统的性能,或者根据这些信息调整系统的行为。 收集你想要的数据,分析出有价值的信息,可以提高系统、产品的安全性,可以帮助开发完善代码,优化产品。 ...
1 2 3 4 5 ... 20
收藏数 1,080,405
精华内容 432,162
关键字:

日志系统