当前位置: 首页 > 后端技术 > Node.js

Node框架接入ELK实践总结_0

时间:2023-04-03 22:32:10 Node.js

本文由Cloud+Community发布作者:J2X我们都有上机查日志的经历。定位现有网络问题带来了巨大的挑战。同时,我们无法对服务框架的各项指标进行有效的量化诊断,更谈不上有针对性的优化和改进。这时,构建一个具有信息查询、服务诊断、数据分析等功能的实时日志监控系统就显得尤为重要。ELK(ELKStack:ElasticSearch、LogStash、Kibana、Beats)是成熟的日志解决方案,其开源和高性能被各大公司广泛采用。而我们业务使用的服务框架是如何对接ELK系统的呢?业务背景我们的业务框架背景:业务框架是一个基于NodeJs的WebServer服务。使用winston日志模块将日志本地化服务产生的日志存放在每台机器的磁盘上。服务部署在不同地域,多台机器连接。整个框架的衔接步骤ELK的接入简单概括为以下几个步骤:日志结构设计:将传统的纯文本日志转化为结构化对象,输出为JSON。日志收集:在框架请求生命周期的一些关键节点输出日志ES索引模板定义:建立从JSON到ES实际存储的映射1.日志结构设计传统上,我们在输出日志时,直接输出日志级别(level)和日志内容字符串(消息)。但是,我们不仅要关注发生的时间和内容,还可能需要关注类似日志发生了多少次,日志的详细信息和上下文,以及关联的日志。因此,我们不仅简单地将我们的日志结构化为一个对象,还提取了日志的关键字段。1.将日志抽象为事件我们将每条日志的发生抽象为一个事件。Event包含:event元字段事件发生时间:datetime、timestamp事件级别:level,例如:ERROR、INFO、WARNING、DEBUG事件名称:event,例如:client-request事件的相对时间(单位:纳秒):reqLife,该字段是事件相对于请求开始的时间(间隔)。事件的位置:行,代码位置;服务器,服务器的位置。请求元字段requestuniqueID:reqId,该字段贯穿整个请求链接上发生的所有事件RequestuserID:reqUid,该字段为用户ID,可以跟踪用户的访问或请求中不同类型的事件链接数据字段。需要输出的细节不同。我们会把这些细节(非元字段)放入d——数据之中。这使得我们的事件结构更加清晰,同时,防止数据字段污染元字段。例如比如client-init事件,服务器端每次收到用户请求都会打印这个事件。我们会把用户的ip、url等事件唯一的放入data字段,放在d对象中。对于一个完整的例子{"datetime":"2018-11-0721:38:09.271","timestamp":1541597889271,"level":"INFO","event":"client-init","reqId":"rJtT5we6Q","reqLife":5874,"reqUid":"999793fc03eda86","d":{"url":"/","ip":"9.9.9.9","httpVersion":"1.1","method":"GET","userAgent":"Mozilla/5.0(Macintosh;IntelMacOSX10_14_0)AppleWebKit/537.36(KHTML,likeGecko)Chrome/70.0.3538.77Safari/537.36","headers":"*"},"浏览器":"{"name":"Chrome","version":"70.0.3538.77","major":"70"}","engine":"{"version":"537.36","name":"WebKit"}","os":"{"name":"MacOS","version":"10.14.0"}","content":"(Empty)","line":"middlewares/foo.js:14","server":"127.0.0.1"}一些字段,比如:browser,os,engine为什么在外层?有时我们希望日志尽可能平坦(最大深度为2)以避免ES不必要的索引带带来性能损失。在实际输出中,我们会将深度大于1的值作为字符串输出。而且有时候有些对象字段是我们关心的,所以我们把这些特殊的字段放在外层,保证输出深度不大于2的原则。一般我们在打印日志的时候,只需要关注事件名称和数据字段即可。另外,我们可以通过打印日志的方式,通过访问上下文统一获取、计算、输出。2.日志改造输出我们提到了如何定义一个日志事件,那么如何在现有的日志方案的基础上进行升级,同时兼容老代码的日志调用方式。升级关键节点的日志//改造前logger.info('client-init=>'+JSON.stringfiy({url,ip,browser,//...}));//改造后logger.info({event:'client-init',url,ip,浏览器,//...});兼容旧的日志调用方式logger.debug('checkLogin');因为winston的log方法本身是支持传递字符串或者对象输入法的,所以对于老的字符串输入法,formatter实际上接收的是{level:'debug',message:'checkLogin'}。formatter是winston在日志输出之前调整日志格式的一个过程,它让我们有机会把这种调用方式输出的日志转换成日志输出之前的纯输出事件——我们称之为raw-logevents,调用方法不需要修改。修改日志输出格式前面提到,winston在输出日志之前,会经过我们预定义的formatter,所以除了兼容逻辑的处理之外,我们可以把一些通用的逻辑放在这里进行处理。在通话中,我们只需要关注领域本身。元字段提取和处理字段长度控制兼容性逻辑处理如何提取元字段,这涉及到context的创建和使用,这里简单介绍一下domain的创建和使用。//---中间件/http-context.jsconstdomain=require('domain');constshortid=require('shortid');module.exports=(req,res,next)=>{constd=domain.创造();d.id=shortid.generate();//要求;d.req=请求;//...res.on('finish',()=>process.nextTick(()=>{d.id=null;d.req=null;d.exit();});d.run(()=>next());}//---app.jsapp.use(require('./middlewares/http-context.js'));//---formatter.jsif(process.domain){reqId=process.domain.id;}这样我们就可以为一个请求中的所有事件输出reqId,从而达到关联事件的目的。2.日志采集现在,我们知道了如何输出一个事件,那么在下一步中,我们要考虑两个问题:我们要把事件输出到哪里?事件应该输出什么细节?也就是说,在整个请求环节中,我们关注了哪些节点,如果出现问题,可以通过哪个节点的信息来快速定位问题?除此之外,我们还有哪些节点可以用来做统计分析呢?结合常见的请求环节(用户请求、服务端接收请求、服务请求下游服务器/数据库(*多次)、数据聚合渲染、服务响应),如下图流程图,我们可以定义我们的Events:用户请求client-init:框架收到请求时打印(未解析),包括:请求地址、请求头、Http版本和方法、用户IP和浏览器client-request:框架收到请求(已解析)时打印,包括:请求地址、请求头、cookie、请求包体client-response:打印在框架返回请求上,包括:请求地址、响应码、响应头、响应包体下游依赖http-start:打印在请求下游Start:requestaddress,requestbody,modulealias(方便的根据名字聚合和域名)http-success:打印在请求上并返回200:请求地址,requestbody,responsebody(code&msg&data),耗时http-error:请求返回非200即连接服务器失败时打印:请求地址、请求体、响应体(代码&消息&堆栈),耗时。http-timeout:请求连接超时打印:请求地址,请求体,响应体(code&msg&stack),耗时。领域那么多,如何选择?总之,事件输出的字段原则是:输出你关心的字段,方便检索,方便后期聚合。一些建议下游请求体和返回体具有固定格式,例如Input:{action:'getUserInfo',payload:{}}Output:{code:0,msg:'',data:{}}我们可以通过事件输出action,code等,让指标和聚合稍后可以通过操作检索某个模块的特定接口。一些原则确保输出字段类型是一致的。由于所有的事件都存储在同一个ES索引中,所以无论是同一个事件还是不同的事件,同一个字段应该是一致的。例如:code不能既是数字又是字符串,这样可能会发生字段冲突,导致某些记录(文档)不能被冲突的字段检索到。ES存储类型为关键字,不能超过ES映射设置的ignore_above中指定的字节数(默认4096字节)。否则,也可能无法找回。3.ES索引模板定义这里引入ES的两个概念,映射(Mapping)和模板(Template)。首先大致罗列一下ES的基本存储类型。String有以下几种类型:keyword&textNumeric:long,integer,doubleDate:dateBoolean:boolean一般我们不需要在ES中显示指定每个事件字段对应的存储类型,ES会自动判断存储类型根据该字段在文档中首次出现的位置的值,对该索引中的该字段进行排序。但有时,我们需要显示和指定某些字段的存储类型。这时候我们就需要定义这个索引的Mapping来告诉ES如何存储和索引这个字段。例如还记得事件元字段中有一个名为timestamp的字段吗?其实我们输出的时候,timestamp的值是一个数字,表示从1970/01/0100:00:00算起的毫秒数,我们希望它在ES中的存储类型是date类型,方便后面的检索和为了可视化,当我们创建索引时,我们指定我们的Mapping。PUTmy_logs{"mappings":{"_doc":{"properties":{"title":{"type":"date","format":"epoch_millis"},}}}}但总的来说,我们可能假设我们的索引名称的格式为my_logs_yyyyMMdd(例如my_logs_20181030),将按日期自动生成日志索引。然后我们需要定义一个模板(Template),它会在创建(匹配)索引时自动套用预设的Mapping。PUT_template/my_logs_template{"index_patterns":"my_logs*","mappings":{"_doc":{"properties":{"title":{"type":"date","format":"epoch_millis"},}}}}Tips:将所有日期产生的日志存储在一个索引中,不仅带来不必要的性能开销,也不利于定期删除较旧的日志。总结至此,日志转换和访问的准备工作已经完成。我们只需要在机器上安装FileBeat——一个轻量级的文件日志Agent,负责将日志文件中的日志传输给ELK。接下来,我们可以使用Kibana来快速检索我们的日志。本文已由腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们的腾讯云技术社区-云家社区公众号和知乎代理号

猜你喜欢