REST服务中的日志可视化(关键技术实现)

引言

在系统构建完成之后,我们通常会使用REST API对外提供服务,在REST API的处理过程中经常会出现一些异想不到的问题(用户权限不足、参数不全、数据库访问异常等),导致请求失败,很多时候用户并不能理解这些失败是如何造成的,他们更多的是直接找到相应的开发者询问:“我的这个接口失败了,没有拿到数据,帮忙看一下吧”,更为复杂的是当我们询问其他用户的时候,他们却说:“你这个接口是正常的啊”,开发者这时就很郁闷:“我又没对你做特殊处理,怎么别人是好的,偏偏就你的失败”(本人在工作初期,经常遇到此类场景,表示很无奈)。

问题还是要解决,怎么办?找日志呗!打开终端、拿起微盾、输入密码、输入IP,等等!!!我们的REST API是部署在多台服务器上,前面有一台Nginx,“我去,谁知道是哪台服务器处理你的请求的,哦,我们在日志目录下挂载了NFS,每个REST API的日志信息会写入到一个固定的文件夹中”,“你什么时候请求这个接口的”,然后估计一个大概的时间,找到对应的日志文件(日志输出使用Log4j,并按照天进行滚动),“我X,这个接口访问如此频繁,日志全部都混在了一起,怎么找”,“对了,我们有RequestID,每个请求都是唯一的,快找”,“我X,RequestID并不是每行日志都带有的,完了”,日志无法定位,无颜面对用户呀,只好羞愧地对用户说:“我们有测试环境,我给你一个IP,你再访问一次,我看看日志就知道啥原因了”。

多么高大上的回答呀,还是掩饰不住尴尬的内心,唉...

问题终归是要解决的,把程序员(尤其是我)搞的不耐烦了,就要有创新了(自夸一下),另外得知我们的小伙伴们提供ELK服务,REST API日志可视化的想法油然而生。

注:本文仅关注在REST环境下如果自定义日志输出,不涉及ELK部分(其他小伙伴支持,团队的力量)

关键问题

REST API中使用log4j进行日志输出,如何在不影响现有代码的基础上(无须在业务代码中添加任何代码),收集一次请求的日志信息,透明的将日志输出至ELK

解决思路

(1)使用ServletRequestListener对请求过程进行监听,请求过程包含两部分:requestInitialized、requestDestroyed;
(2)每一次请求的初始化、处理、销毁是由一个独立的线程负责完成的(熟悉Java的同学可能立马就会想到ThreadLocal);
(3)现有业务代码中已经使用log4j作日志输出,为了保证不影响现有代码及以后的开发,唯一的方式,自定义Appender;

通过上述三步,我们可以大致得出这样一个流程:

(1)在ServletRequestListener requestInitialized初始化当前线程的日志对象;
(2)业务代码执行过程中,log4j输出日志时通过我们自定义的Appender,将日志信息保存至当前线程的日志对象中;
(3)在ServletRequestListener requestDestroyed中将当前线程的日志对象中的日志信息输出至目的地(这里是ELK,也可以是其它),然后清空线程对象。

注意:以上全部操作均依赖于一个请求过程的处理全部处于一个线程环境中。

解决方案

(1)定义日志对象

public class DipLog {

    public static class DipLogMessage {

        private String time;

        private String level;

        private String filename;

        private String className;

        private String methodName;

        private String lineNumber;

        private String message;

        public DipLogMessage(String time, String level, String filename,
                String className, String methodName, String lineNumber,
                String message) {
            this.time = time;

            this.level = level;

            this.filename = filename;

            this.className = className;

            this.methodName = methodName;

            this.lineNumber = lineNumber;

            this.message = message;
        }

        public String getTime() {
            return time;
        }

        public String getLevel() {
            return level;
        }

        public String getFilename() {
            return filename;
        }

        public String getClassName() {
            return className;
        }

        public String getMethodName() {
            return methodName;
        }

        public String getLineNumber() {
            return lineNumber;
        }

        public String getMessage() {
            return message;
        }

        @Override
        public String toString() {
            return String.format("%s\t%s\t%s\t%s\t%s\t%s\t%s", time, level,
                    filename, className, methodName, lineNumber, message);
        }

    }

    private Map<String, String> properties = new HashMap<String, String>();

    private List<DipLogMessage> messages = Collections
            .synchronizedList(new ArrayList<DipLog.DipLogMessage>());

    public void addProperty(String key, String value) {
        properties.put(key, value);
    }

    public void addMessage(DipLogMessage message) {
        messages.add(message);
    }

    public Map<String, String> getProperties() {
        return properties;
    }

    public List<DipLogMessage> getMessages() {
        return messages;
    }

}

properties中保存一些自定义属性值(log4j本身不支持的),messages中保存通过log4j debug、info、warn、error输出的日志消息(DipLogMessage )。

(2)通过ThreadLocal保存一个请求处理过程中的日志对象

public class DipLogThreadLocal {

    private static final ThreadLocal<DipLog> DIP_LOG_THREAD_LOCAL = new ThreadLocal<DipLog>();

    public static DipLog get() {
        return DIP_LOG_THREAD_LOCAL.get();
    }

    public static void set(DipLog dipLog) {
        DIP_LOG_THREAD_LOCAL.set(dipLog);
    }

    public static void clear() {
        DIP_LOG_THREAD_LOCAL.set(null);
    }

}

(3)扩展Log4j,自定义Appender,将请求处理过程中的日志消息保存至当前线程关联的日志对象中

public class DipLogAppender extends WriterAppender {

    private static final SimpleDateFormat DATETIME_FORMAT = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss,SSS");

    @Override
    public void append(LoggingEvent event) {
        String time = DATETIME_FORMAT.format(new Date(event.getTimeStamp()));

        String level = event.getLevel().toString();

        String filename = event.getLocationInformation().getFileName();

        String className = event.getLocationInformation().getClassName();

        String methodName = event.getLocationInformation().getMethodName();

        String lineNumber = event.getLocationInformation().getLineNumber();

        String message = event.getRenderedMessage();

        DipLogMessage dipLogMessage = new DipLogMessage(time, level, filename,
                className, methodName, lineNumber, message);

        DipLog dipLog = DipLogThreadLocal.get();

        dipLog.addMessage(dipLogMessage);
    }

}

(4)创建ServletRequestListener

public class DipLogRequestListener implements ServletRequestListener {

    @Override
    public void requestInitialized(ServletRequestEvent event) {
        DipLog dipLog = new DipLog();

        dipLog.addProperty("requestId",
                String.valueOf(System.currentTimeMillis()));

        dipLog.addProperty("url", ((HttpServletRequest) event
                .getServletRequest()).getRequestURI());

        DipLogThreadLocal.set(dipLog);
    }

    @Override
    public void requestDestroyed(ServletRequestEvent event) {
        DipLog dipLog = DipLogThreadLocal.get();

        Map<String, String> properties = dipLog.getProperties();

        for (Entry<String, String> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + "\t" + entry.getValue());
        }

        List<DipLogMessage> messages = dipLog.getMessages();

        for (DipLogMessage dipLogMessage : messages) {
            System.out.println(dipLogMessage);
        }

        DipLogThreadLocal.clear();
    }

}

从黑体部分代码可以看出,我们在日志对象中保存着当前请求的RequestID及URL(可以添加到最后的日志消息输出),通过requestDestroyed完成请求日志消息的具体输出(这里仅仅模拟,直接输出至控制台)。

这里仅仅介绍核心实现,可以在此基础之上根据业务需求扩展出更为复杂的功能。

时间: 2024-10-10 13:10:40

REST服务中的日志可视化(关键技术实现)的相关文章

接口服务中的日志

先来看下日志对于一个接口服务的作用: 监控服务的状态,一般程序中会增加一些跟踪或者提示性的日志,用来判断服务执行的详细情况,特别是执行一些复合功能的接口或者叫聚合接口非常有利于我们判断接口的执行情况 安全,用来分析调用者的身份信息,防止接口被非法恶意调用等 性能,可以统计每个接口的访问执行时间来分析系统的瓶颈 异常分析,对于线上的异常,在没有debug的环境下,要想分析问题原因最有价值的就要算异常的堆栈信息 上面的这几点需求,如果接口服务使用的是一些高级产品比如dubbo,其实它已经实现了大部分

iOS中 加强日志输出 开发技术总结

对于那些做后端开发的工程师来说,看LOG解Bug应该是理所当然的事,但我接触到的移动应用开发的工程师里面,很多人并没有这个意识,查Bug时总是一遍一遍的试图重现,试图调试,特别是对一些不太容易重现的Bug经常焦头烂额.而且iOS的异常机制比较复杂,Objective-C的语言驾驭也需要一定的功力,做出来的应用有时候挺容易产生崩溃闪退.一遍一遍的用XCode取应用崩溃记录.解析符号,通常不胜其烦,有时还对着解析出来的调用栈发呆,因为程序当时的内部状态常常难以看明白,只能去猜测. 对于真机,日志没法

使用日志服务进行Kubernetes日志采集

阿里云容器服务Kubernetes集群集成了日志服务(SLS),您可在创建集群时启用日志服务,快速采集Kubernetes 集群的容器日志,包括容器的标准输出以及容器内的文本文件. 新建 Kubernetes 集群 如果您尚未创建任何的 Kubernetes 集群,可以按照本节的步骤来进行操作: 登录 容器服务管理控制台. 单击左侧导航栏中集群,单击右上角创建Kubernetes集群. 进入创建页面后,参见创建Kubernetes集群进行配置. 拖动到页面底部,勾选日志服务配置项,表示在新建的

Java中,多态的实现有哪些要求?实现多态的关键技术?

多态指的是允许不同类的对象对同一消息做出响应,即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用).实现多态的方法是动态绑定( Dynamic Binding),动态绑定指的是在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法. 在Java语言中, Override(覆盖.重写)是实现多态的关键技术,在子类中定义与父类相同的方法,同时有自己不同于父类的实现,在使用的时候可以用父类的引用指向不同的子类,从而在运行时决定调用哪个子类的方法. 多态的实现有

大数据流式计算:关键技术及系统实例

孙大为1, 张广艳1,2, 郑纬民1 摘要:大数据计算主要有批量计算和流式计算两种形态,目前,关于大数据批量计算系统的研究和讨论相对充分,而如何构建低延迟.高吞吐且持续可靠运行的大数据流式计算系统是当前亟待解决的问题且研究成果和实践经验相对较少.总结了典型应用领域中流式大数据所呈现出的实时性.易失性.突发性.无序性.无限性等特征,给出了理想的大数据流式计算系统在系统结构.数据传输.应用接口.高可用技术等方面应该具有的关键技术特征,论述并对比了已有的大数据流式计算系统的典型实例,最后阐述了大数据流

互联网DSP广告系统架构及关键技术解析

互联网DSP广告系统架构及关键技术解析 宿逆 关注 1.9 2017.10.09 17:05* 字数 8206 阅读 10271评论 2喜欢 60 广告和网络游戏是互联网企业主要的盈利模式 广告是广告主通过媒体以尽可能低成本的方式与用户达成接触的商业行为.也就是说按照某种市场意图接触相应人群,影响其中潜在用户,使其选择广告主产品的几率增加,或对广告主品牌产生认同,通过长期的影响逐步形成用户对品牌的转化. 一个好的DSP系统需要满足: 拥有强大的RTB(Real-Time Bidding)的基础设

Java Hotspot G1 GC的一些关键技术

G1 GC,全称Garbage-First Garbage Collector,通过-XX:+UseG1GC参数来启用,作为体验版随着JDK 6u14版本面世,在JDK 7u4版本发行时被正式推出,相信熟悉JVM的同学们都不会对它感到陌生.在JDK 9中,G1被提议设置为默认垃圾收集器(JEP 248).在官网中,是这样描述G1的: The Garbage-First (G1) collector is a server-style garbage collector, targeted for

HyperLedger Fabric关键技术(6.4)

本节介绍从最底层的账本开始,逐一讲解账本的结构和存储.智能合约的编写和部署.通道的操作.节点的背书和提交.排序的共识和客户端SDK的接口调用,与交易流程顺序相反,由里及表的说明Fabric最关键的技术,通过学习了这六种关键技术知识,能初步掌握Fabric的核心,了解Fabric运作原理. 6.3.1 帐本(Ledger) Fabric帐本(Ledger)是一系列有序和防篡改的状态转换的记录,结构由一个区块链构成,并将不可变的.有序的记录存放在区块中:同时包含一个状态数据库来记录当前的状态,账本的

Go 开发关键技术指南 | Go 面向失败编程 (内含超全知识大图)

作者 |?杨成立(忘篱) 阿里巴巴高级技术专家 关注"阿里巴巴云原生"公众号,回复 Go 即可查看清晰知识大图! 导读:从问题本身出发,不局限于 Go 语言,探讨服务器中常常遇到的问题,最后回到 Go 如何解决这些问题,为大家提供 Go 开发的关键技术指南.我们将以系列文章的形式推出<Go 开发的关键技术指南>,共有 4 篇文章,本文为第 2 篇. Could Not Recover 在 C/C++ 中, 最苦恼的莫过于上线后发现有野指针或内存越界,导致不可能崩溃的地方崩溃