이 글의 내용은 Node 프레임워크를 ELK에 연결하는 과정을 요약한 것입니다. 참고할 만한 가치가 있으니 도움이 필요한 분들에게 도움이 되길 바랍니다.
우리 모두는 클러스터 수가 증가하면 이러한 원시적인 작업으로 인한 비효율성으로 인해 기존 네트워크 문제를 찾는 데 큰 어려움을 겪을 뿐만 아니라 이를 수행하는 것도 불가능하게 됩니다. 목표한 최적화 및 개선은 물론, 서비스 프레임워크의 다양한 지표에 대한 효과적인 정량적 진단을 수행하는 것도 불가능합니다. 이때 정보검색, 서비스 진단, 데이터 분석 등의 기능을 갖춘 실시간 로그 모니터링 시스템을 구축하는 것이 특히 중요하다.
ELK(ELK 스택: ElasticSearch, LogStash, Kibana, Beats)는 오픈 소스와 높은 성능을 갖춘 성숙한 로깅 솔루션입니다. 우리 사업에서 사용하는 서비스 프레임워크는 ELK 시스템과 어떻게 연결되나요?
비즈니스 프레임워크 배경:
비즈니스 프레임워크는 NodeJs WebServer를 기반으로 합니다.
서비스는 Winston 로그 모듈을 사용하여 로그를 현지화합니다.
서비스에서 생성된 로그가 저장됩니다.
서비스는 서로 다른 지역의 여러 머신에 배포됩니다
전체 프레임워크를 ELK에 다음 단계로 간단히 요약하겠습니다.
로그 구조 설계 : 전통적인 일반 텍스트로 구성 로그는 구조화된 객체로 변경되어 JSON으로 출력됩니다.
로그 수집: 프레임워크 요청 수명 주기의 일부 주요 노드에서 로그를 출력합니다.
ES 인덱스 템플릿 정의: JSON에서 매핑 설정 to ES 실제 저장소
전통적으로 우리는 로그 출력을 할 때 로그 레벨(level)과 로그 내용 문자열(message)을 직접 출력했습니다. 그러나 우리는 언제, 무슨 일이 일어났는지에만 주의를 기울이는 것이 아니라, 유사한 로그가 몇 번이나 발생했는지, 로그의 내용과 맥락, 관련 로그에도 주의를 기울여야 할 수도 있습니다. 따라서 우리는 단순히 로그를 객체로 구성할 뿐만 아니라 로그의 주요 필드도 추출합니다.
각 로그의 발생을 이벤트로 추상화합니다. 이벤트에는 다음이 포함됩니다.
이벤트 발생 시간: 날짜시간, 타임스탬프
이벤트 수준: 레벨, 예: ERROR, INFO, WARNING, DEBUG
이벤트 이름: 이벤트, 예: client-request
이벤트 발생 상대 시간(단위: 나노초): reqLife, 이 필드는 요청과 관련된 이벤트가 발생하기 시작하는 시간(간격)입니다.
이벤트 위치: 라인, 서버 코드 위치, 서버 위치
요청 고유 ID: reqId, 이 필드는 전체 요청 링크에서 발생하는 모든 이벤트를 통해 실행됩니다.
요청 사용자 ID: reqUid, 이 필드는 사용자의 액세스 또는 링크 요청
다양한 유형의 이벤트를 출력하려면 다양한 세부정보가 필요합니다. 이러한 세부정보(비메타 필드)를 d -- 데이터에 넣습니다. 이를 통해 이벤트 구조가 더 명확해지고 동시에 데이터 필드가 메타 필드를 오염시키는 것을 방지할 수 있습니다.
예: 서버가 사용자 요청을 받을 때마다 인쇄되는 client-init 이벤트와 같이 사용자의 IP, URL 및 기타 이벤트를 데이터 필드에 고유하게 분류합니다. object中
완전한 예 제공
{ "datetime":"2018-11-07 21: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; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36", "headers":"*" }, "browser":"{"name":"Chrome","version":"70.0.3538.77","major":"70"}", "engine":"{"version":"537.36","name":"WebKit"}", "os":"{"name":"Mac OS","version":"10.14.0"}", "content":"(Empty)", "line":"middlewares/foo.js:14", "server":"127.0.0.1" }
일부 필드(예: browser, os,engine)때때로 로그가 외부 레이어에서 가능한 한 평평해지기를 원하는 이유는 무엇입니까? (최대 깊이는 2), ES에서 불필요한 인덱스로 인한 성능 손실을 방지합니다. 실제 출력에서는 깊이가 1보다 큰 값을 문자열로 출력합니다. 때때로 일부 객체 필드가 우리에게 중요하므로 이러한 특수 필드를 외부 레이어에 배치하여 출력 깊이가 2보다 크지 않도록 합니다.
일반적으로 로그를 출력할 때 이벤트 이름
과 데이터 필드
에만 주의하면 됩니다. 또한, 로그를 출력하는 방식으로 context에 접근하여 일률적으로 획득, 계산, 출력할 수 있다. 事件名称
及数据字段
即可。其他,我们可以在打印日志的方法中,通过访问上下文统一获取,计算,输出。
前面我们提到了如何定义一个日志事件, 那么,我们如何基于已有日志方案做升级,同时,兼容旧代码的日志调用方式。
// 改造前 logger.info('client-init => ' + JSON.stringfiy({ url, ip, browser, //... })); // 改造后 logger.info({ event: 'client-init', url, ip, browser, //... });
logger.debug('checkLogin');
因为 winston 的 日志方法本身就支持 string 或者 object 的传入方式, 所以对于旧的字符串传入写法,formatter 接收到的实际上是{ level: 'debug', message: 'checkLogin' }。formatter 是 winston 的日志输出前调整日志格式的一道工序, 这一点使我们在日志输出前有机会将这类调用方式输出的日志,转为一个纯输出事件 -- 我们称它们为raw-log事件,而不需要修改调用方式。
前面提到 winston 输出日志前,会经过我们预定义的formatter,因此除了兼容逻辑的处理外,我们可以将一些公共逻辑统一放在这里处理。而调用上,我们只关注字段本身即可。
元字段提取及处理
字段长度控制
兼容逻辑处理
如何提取元字段,这里涉及上下文的创建与使用,这里简单介绍一下 domain 的创建与使用。
//--- middlewares/http-context.js const domain = require('domain'); const shortid = require('shortid'); module.exports = (req, res, next) => { const d = domain.create(); d.id = shortid.generate(); // reqId; d.req = req; //... res.on('finish', () => process.nextTick(() => { d.id = null; d.req = null; d.exit(); }); d.run(() => next()); } //--- app.js app.use(require('./middlewares/http-context.js')); //--- formatter.js if (process.domain) { reqId = process.domain.id; }
这样,我们就可以将 reqId
PUT my_logs { "mappings": { "_doc": { "properties": { "title": { "type": "date", "format": "epoch_millis" }, } } } }
PUT _template/my_logs_template { "index_patterns": "my_logs*", "mappings": { "_doc": { "properties": { "title": { "type": "date", "format": "epoch_millis" }, } } } }
메타필드 추출 및 처리
필드 길이 제어#🎜 🎜 #
호환 논리 처리
이런 방식으로 요청의 모든 이벤트에 reqId
를 출력하여 이벤트 상관 목적을 달성할 수 있습니다.
2. 로그 수집
이제 이벤트 출력 방법을 알았으니 다음 단계에서는 두 가지 문제를 고려해야 합니다.
http-start: 打印于请求下游起始:请求地址,请求包体,模块别名(方便基于名字聚合而且域名)
http-success: 打印于请求返回 200:请求地址,请求包体,响应包体(code & msg & data),耗时
http-error: 打印于请求返回非 200,亦即连接服务器失败:请求地址,请求包体,响应包体(code & message & stack),耗时。
http-timeout: 打印于请求连接超时:请求地址,请求包体,响应包体(code & msg & stack),耗时。
字段这么多,该怎么选择? 一言以蔽之,事件输出的字段原则就是:输出你关注的,方便检索的,方便后期聚合的字段。请求下游的请求体和返回体有固定格式, e.g. 输入:{ action: 'getUserInfo', payload: {} } 输出: { code: 0, msg: '', data: {}} 我们可以在事件输出 action,code 等,以便后期通过 action 检索某模块具体某个接口的各项指标和聚合。
保证输出字段类型一致 由于所有事件都存储在同一个 ES 索引, 因此,相同字段不管是相同事件还是不同事件,都应该保持一致,例如:code不应该既是数字,又是字符串,这样可能会产生字段冲突,导致某些记录(document)无法被冲突字段检索到。
ES 存储类型为 keyword, 不应该超过 ES mapping 设定的 ignore_above 中指定的字节数(默认4096个字节)。否则同样可能会产生无法被检索的情况
这里引入 ES 的两个概念,映射(Mapping)与模版(Template)。
首先,ES 基本的存储类型大概枚举下,有以下几种
String: keyword & text
Numeric: long, integer, double
Date: date
Boolean: boolean
一般的,我们不需要显示指定每个事件字段的在ES对应的存储类型,ES 会自动根据字段第一次出现的document中的值来决定这个字段在这个索引中的存储类型。但有时候,我们需要显示指定某些字段的存储类型,这个时候我们需要定义这个索引的 Mapping, 来告诉 ES 这此字段如何存储以及如何索引。
e.g.
还记得事件元字段中有一个字段为 timestamp ?实际上,我们输出的时候,timestamp 的值是一个数字,它表示跟距离 1970/01/01 00:00:00 的毫秒数,而我们期望它在ES的存储类型为 date 类型方便后期的检索和可视化, 那么我们创建索引的时候,指定我们的Mapping。
PUT my_logs { "mappings": { "_doc": { "properties": { "title": { "type": "date", "format": "epoch_millis" }, } } } }
但一般的,我们可能会按日期自动生成我们的日志索引,假定我们的索引名称格式为 my_logs_yyyyMMdd (e.g. my_logs_20181030)。那么我们需要定义一个模板(Template),这个模板会在(匹配的)索引创建时自动应用预设好的 Mapping。
PUT _template/my_logs_template { "index_patterns": "my_logs*", "mappings": { "_doc": { "properties": { "title": { "type": "date", "format": "epoch_millis" }, } } } }
小结
至此,日志改造及接入的准备工作都已经完成了,我们只须在机器上安装 FileBeat -- 一个轻量级的文件日志Agent, 它负责将日志文件中的日志传输到 ELK。接下来,我们便可使用 Kibana 快速的检索我们的日志。
위 내용은 Node Framework를 ELK에 연결하는 과정 요약의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!