Maison  >  Article  >  interface Web  >  Résumé du processus de connexion du framework Node à ELK

Résumé du processus de connexion du framework Node à ELK

不言
不言avant
2018-11-21 11:23:212391parcourir

Le contenu de cet article est un résumé du processus de connexion du framework Node à ELK. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.

Nous avons tous l'expérience de la vérification des journaux sur les machines. Lorsque le nombre de clusters augmente, l'inefficacité apportée par cette opération primitive ne nous pose pas seulement de grands défis dans la localisation des problèmes de réseau existants. Dans le même temps, nous sommes incapables de réaliser un diagnostic quantitatif efficace des divers indicateurs de notre cadre de services, sans parler d'une optimisation et d'une amélioration ciblées. À l'heure actuelle, il est particulièrement important de créer un système de surveillance des journaux en temps réel avec des fonctions telles que la recherche d'informations, le diagnostic de service et l'analyse des données.

ELK (ELK Stack : ElasticSearch, LogStash, Kibana, Beats) est une solution de journalisation mature. Son open source et ses hautes performances sont largement utilisées dans les grandes entreprises. Comment le cadre de service utilisé par notre entreprise se connecte-t-il au système ELK ?

Contexte commercial

Notre expérience en matière de cadre commercial :

  • Le cadre commercial est un serveur Web basé sur NodeJs

  • Le service utilise le module winston log pour localiser les logs

  • Les logs générés par le service sont stockés sur les disques des machines respectives

  • Les services sont déployés dans différentes régions Machines multiples

Étapes d'accès

Nous résumons simplement l'ensemble du framework à ELK dans les étapes suivantes :

  • Conception de la structure des journaux : modifiez les journaux traditionnels en texte brut en objets structurés et générez-les au format JSON.

  • Collecte de journaux : publiez des journaux à certains nœuds clés du cycle de vie de la demande de framework

  • Définition du modèle d'index ES : établissement d'un mappage du JSON au stockage réel ES

Conception de la structure des journaux

Traditionnellement. , nous sommes Lors de la sortie du journal, le niveau de journal (niveau) et la chaîne de contenu du journal (message) sont directement affichés. Cependant, nous ne prêtons pas seulement attention au moment et à ce qui s'est produit, mais nous devrons peut-être également prêter attention au nombre de fois où des journaux similaires se sont produits, aux détails et au contexte des journaux, ainsi qu'aux journaux associés. Par conséquent, nous structurons non seulement simplement nos journaux en objets, mais nous extrayons également les champs clés des journaux.

1. Résumé des journaux dans les événements

Nous résumons l'occurrence de chaque journal en tant qu'événement. L'événement contient :

Métachamp de l'événement

Heure d'occurrence de l'événement : datetime, horodatage

Niveau de l'événement : niveau, par exemple : ERREUR, INFO, AVERTISSEMENT, DEBUG

Nom de l'événement : event, Par exemple : client-request

Temps relatif auquel l'événement se produit (unité : nanoseconde) : reqLife, ce champ est l'heure (intervalle) à laquelle l'événement commence à se produire par rapport à la requête

L'emplacement où l'événement se produit : ligne, emplacement du code serveur, l'emplacement du serveur

Champ méta de la requête

ID unique de la demande : reqId, ce champ parcourt toute la demande Tous les événements qui se produisent sur le lien

Demande d'ID utilisateur : reqUid, ce champ est l'ID utilisateur, qui peut suivre l'accès de l'utilisateur ou demander le lien

Champ de données

Différents types d'événements nécessitent la sortie de différents détails. Nous mettons ces détails (champs non méta) dans d -- data. Cela rend notre structure d'événements plus claire et empêche en même temps les champs de données de contaminer les méta-champs.

Par exemple, comme l'événement client-init, cet événement sera imprimé chaque fois que le serveur reçoit une demande de l'utilisateur. Nous classerons de manière unique l'adresse IP, l'URL et les autres événements de l'utilisateur dans des champs de données et les placerons dans l'objet d.

Donnez un exemple complet

{
    "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"
}

Certains champs, tels que : navigateur, système d'exploitation, moteur Pourquoi souhaitons-nous parfois que le journal soit le plus plat possible dans la couche externe (maximum. la profondeur est de 2) pour éviter ES et non la perte de performances causée par l'indexation nécessaire. Dans la sortie réelle, nous afficherons les valeurs avec une profondeur supérieure à 1 sous forme de chaînes. Parfois, certains champs d'objet nous préoccupent, nous plaçons donc ces champs spéciaux dans la couche externe pour nous assurer que la profondeur de sortie n'est pas supérieure à 2.

Généralement, lorsque nous imprimons le journal, nous devons seulement faire attention à 事件名称 et 数据字段. De plus, nous pouvons obtenir, calculer et sortir uniformément en accédant au contexte dans la méthode d'impression des journaux.

2. Sortie de la transformation du journal

Nous avons mentionné plus tôt comment définir un événement de journal, alors comment pouvons-nous mettre à niveau en fonction de la solution de journal existante tout en étant compatible avec la méthode d'appel de journal de l'ancien code .

Mettre à niveau les journaux des nœuds clés

// 改造前
logger.info('client-init => ' + JSON.stringfiy({
    url,
    ip,
    browser,
    //...
}));

// 改造后
logger.info({
    event: 'client-init',
    url,
    ip,
    browser,
    //...
});

Compatible avec l'ancienne méthode d'appel de journal

logger.debug('checkLogin');

Parce que la méthode de journalisation de Winston elle-même prend en charge la méthode entrante de chaîne ou d'objet , donc pour l'ancienne méthode d'écriture d'entrée de chaîne, ce que le formateur reçoit est en fait { level: 'debug', message: 'checkLogin' }. Le formateur est un processus d'ajustement du format du journal de Winston avant la sortie du journal. Cela nous donne la possibilité de convertir la sortie du journal par ce type de méthode d'appel en un événement de sortie pur avant la sortie du journal - nous les appelons événements de journal brut, pas besoin de modifier. la méthode d'appel.

Transformer le format de sortie du journal

Comme mentionné précédemment, avant que Winston ne publie le journal, il passera par notre formateur prédéfini, donc en plus du traitement de la logique compatible, nous pouvons mettre quelques éléments communs logique ici. Quant à l'appel, nous nous concentrons uniquement sur le terrain lui-même.

  • Extraction et traitement des méta-champs

  • Contrôle de la longueur du champ

  • Traitement logique compatible

Comment extraire des méta-champs, cela implique la création et l'utilisation de contexte, voici une brève introduction à la création et à l'utilisation de domaine.

//--- 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;
}

De cette façon, nous pouvons afficher reqId tous les événements en une seule requête, atteignant ainsi l'objectif de corrélation des événements.

2. Collecte de journaux

Maintenant que nous savons comment générer un événement, nous devrions considérer deux questions à l'étape suivante :

  1. Nous où l'événement doit-il être généré ?

  2. Quels détails l'événement doit-il produire ?

En d'autres termes, dans l'ensemble du lien de requête, de quels nœuds sommes-nous préoccupés ? Si un problème survient, les informations de quel nœud peuvent être utilisées pour localiser rapidement le problème ? De plus, quelles données de nœuds pouvons-nous utiliser pour l’analyse statistique ?

Combiné avec des liens de requête courants (demande de l'utilisateur, demande de réception côté service, demande de service vers le serveur/base de données en aval (*plusieurs fois), rendu d'agrégation de données, réponse du service), comme indiqué dans l'organigramme ci-dessous

Résumé du processus de connexion du framework Node à ELK

Ensuite, nous pouvons définir notre événement comme ceci :

Demande utilisateur

client-init : Imprime la requête reçue par le frame (non analysé), comprenant : l'adresse de la requête, les en-têtes de la requête, la version et la méthode HTTP, l'adresse IP de l'utilisateur et le navigateur

requête client : imprimé lorsque le cadre reçoit une requête (analysée), comprenant : l'adresse de la requête, les en-têtes de la requête , Cookie, corps du package de demande

réponse-client : imprime la requête renvoyée par le cadre, comprenant : l'adresse de la demande, le code de réponse, l'en-tête de réponse, le corps du package de réponse

下游依赖

http-start: 打印于请求下游起始:请求地址,请求包体,模块别名(方便基于名字聚合而且域名)

http-success: 打印于请求返回 200:请求地址,请求包体,响应包体(code & msg & data),耗时

http-error:  打印于请求返回非 200,亦即连接服务器失败:请求地址,请求包体,响应包体(code & message & stack),耗时。

http-timeout:  打印于请求连接超时:请求地址,请求包体,响应包体(code & msg & stack),耗时。

字段这么多,该怎么选择? 一言以蔽之,事件输出的字段原则就是:输出你关注的,方便检索的,方便后期聚合的字段。

一些建议

  1. 请求下游的请求体和返回体有固定格式, e.g. 输入:{ action: 'getUserInfo', payload: {} } 输出: { code: 0, msg: '', data: {}} 我们可以在事件输出 action,code 等,以便后期通过 action 检索某模块具体某个接口的各项指标和聚合。

一些原则

  1. 保证输出字段类型一致 由于所有事件都存储在同一个 ES 索引, 因此,相同字段不管是相同事件还是不同事件,都应该保持一致,例如:code不应该既是数字,又是字符串,这样可能会产生字段冲突,导致某些记录(document)无法被冲突字段检索到。

  2. ES 存储类型为 keyword, 不应该超过 ES mapping 设定的 ignore_above 中指定的字节数(默认4096个字节)。否则同样可能会产生无法被检索的情况

三、ES 索引模版定义

这里引入 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 快速的检索我们的日志。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer