登录分布式php应用程序


本文将讨论日志记录的好处。 我将介绍有关PSR的日志。 我将添加一些有关使用已记录事件的级别,消息和上下文的个人建议。 将给出一个示例,说明如何在用Laravel编写并通过Docker在多个实例上启动的应用程序中使用ELK组织日志记录和监视。 我将签署警告系统的重要规则。 我将给出一个脚本示例,该脚本使用一个命令来引发整个监视堆栈。


日志记录的好处


井井有条的日志记录至少允许以下内容:


  • 要知道有些事情没有按预期进行(有错误)
  • 了解错误的详细信息,这将有助于说明错误发生的原因和对象,并防止重复发生
  • 要知道一切都按计划进行(access.log,debug级,info级)

日志本身并不能告诉您所有这一切,但是借助日志,可以独立地查找事件的详细信息,或者为日志配置监视系统,以便能够通知问题。 如果日志中的消息带有足够数量的上下文,则这将大大简化调试,因为您将可以访问有关事件发生情况的更多数据。


写什么和写什么


php社区的一部分为一些代码编写任务提出了建议。 一种这样的建议是PSR-3 Logger Interface 。 它仅描述您需要记录的内容。 为此,开发了“ Psr\Log\LoggerInterface / log”软件包的Psr\Log\LoggerInterface 。 使用它时,您需要了解事件的三个​​组成部分:


  1. 级别 -事件重要性
  2. 消息 -描述事件的文字
  3. 上下文 -有关事件的其他信息数组

PSR-3事件级别


这些级别是从RFC 5424-Syslog协议中借用的,其大致描述如下:


  • debug-调试详细信息
  • 信息-有趣的事件
  • 注意-重大事件,但非错误
  • 警告-例外情况,但没有错误
  • 错误-不需要瞬时干预的执行错误
  • 严重-严重情况(系统组件不可用,意外异常)
  • 警报-采取行动需要立即干预
  • 紧急-系统无法运作

有描述,但由于难以确定某些事件的重要性,因此并不总是易于理解。 例如,在单个请求的上下文中,不可能访问连接的资源。 在记录此事件时,我们不知道一个请求是否失败,或者只有一个用户失败。 这取决于是否需要立即进行干预,或者这是否是罕见的情况,它可以等待甚至被忽略。 在监视日志的框架中解决了此类问题。 但是您仍然需要确定级别。 因此,可以商定团队中的日志记录级别。 一个例子:


  • 紧急情况是外部系统的级别,可以查看您的系统并确定它完全不起作用,或者其自我诊断不起作用。
  • 警报 -系统本身可以例如通过计划的任务来诊断其状态,并以此级别记录事件。 它可以是已连接资源的检查,也可以是特定的检查,例如,所用外部资源帐户的余额。
  • 严重的 -发生故障时,如果发生故障的系统组件非常重要且应始终有效,则该事件很重要。 它已经很大程度上取决于系统的功能。 即使只发生一次,也适用于对快速发现很重要的事件。
  • 错误 -发生了一个事件,有关该事件,如果很快重复,您需要报告。 无法完成必须执行的操作,但是此操作不属于关键说明。 例如,无法根据用户的请求保存个人资料图片,但是该系统不是个人资料图片服务,而是聊天系统。
  • 警告 -事件,要立即通知您,您需要在一段时间内拨打大量事件。 无法执行某项操作,该操作的失败不会破坏任何严重的问题。 这些仍然是错误,但可能需要等待工作时间表进行更正。 例如,不可能保存用户的头像,并且该系统是在线商店。 为了了解突发异常,必须(频繁)通知它们,因为它们可能是更严重问题的症状。
  • 注意 -这些事件报告系统提供的偏差,这些偏差是系统正常运行的一部分。 例如,用户在入口处输入了错误的密码,用户没有填写中间名,但这不是必须的,用户购买了0卢布的订单,但这在极少数情况下为您提供。 还需要高频率地通知它们,因为偏差数量的急剧增加可能是紧急需要修复的错误的结果。
  • info-事件,事件的发生报告了系统的正常运行。 例如,用户已注册,用户已购买产品,用户已留下反馈。 此类事件的通知需要以相反的方式进行配置:如果一段时间内发生的此类事件数量不足,则需要进行通知,因为这些错误的减少可能是由于错误导致的。
  • debug-用于调试系统上进程的事件。 当您向事件的上下文中添加足够的数据时,您可以诊断问题,或得出该过程在系统中正常运行的结论。 例如,用户打开了一个产品页面并收到了推荐列表。 显着增加了发送的事件数,因此允许在一段时间后删除此类事件的日志记录。 结果,在正常操作中此类事件的数量将是可变的,因此可以省略对它们的通知监视。

活动讯息


通过PSR-3,消息必须是字符串或带有__toString()方法的对象。 此外,根据PSR-3,消息行可能包含”User {username} created”形式的占位符,可以用上下文数组中的值替换这些占位符。 当使用Elasticsearch和Kibana进行监视时,我建议不要使用占位符,而应该写固定的行,因为这将简化事件的过滤,并且上下文将始终存在。 另外,我建议注意此消息的其他要求:


  1. 案文应简短但有意义。 这是警报中将要出现的内容,并且将在已发生的事件列表中显示。
  2. 对于程序的不同部分,文本最好是唯一的。 这将使警报可以在不查看上下文的情况下了解事件发生在哪个部分。

事件背景


PSR-3的事件上下文是变量值(例如,实体ID)的数组(可能是嵌套的)。 如果有关事件的消息明确,则上下文可以保留为空白。 在记录异常的情况下,您应该传递整个异常,而不仅仅是getMessage() 。 通过NormalizerFormatter使用Monolog时将自动从异常中提取有用的数据并将其添加到事件上下文中,包括堆栈跟踪。 也就是说,您需要


 [ 'exception' => $exception->getMessage(), ] 

使用


 [ 'exception' => $exception, ] 

在Laravel中,您可以自动输入事件中事件的数据。 这可以通过全局日志上下文 (仅对于未成功的异常或通过report() )或通过LogFormatter(对于所有事件)来完成。 通常,信息添加有当前用户的ID,请求URI,IP,请求UUID等。


将Elasticsearch用作日志存储库时,请记住它使用固定的数据类型。 也就是说,如果您在数字的上下文中传递了customer_id ,则当您尝试保存其他类型的事件(例如,字符串(uuid))时,将不会编写此类消息。 首次接收该值时,索引中的类型是固定的。 如果每天创建索引,那么新类型将仅在第二天记录。 但这甚至不能解决所有问题,因为对于Kibana,类型将是混合的,并且与该类型关联的某些操作只有在混合索引后才能使用。


为防止出现此问题,建议您遵循以下规则:


  • 不要使用过于通用的键名,键名可以是不同的类型
  • 如果不确定类型值,请进行显式转换

示例:代替


 [ 'response' => $response->all(), 'customer_id' => $id, 'value' => $someValue, ] 

使用


 [ 'smsc_response_data' => json_encode($response->all()), 'customer_id' => (string) $customer_id, 'smsc_request_some_value' => (string) $someValue, ] 

从代码调用记录器


为了在日志中快速记录一个事件,您可以提供几个选项。 让我们考虑其中的一些。


  1. 声明全局函数log()并从程序的不同部分调用它。 这种方法有许多缺点。 例如,在我们访问该函数的类中,形成了隐式依赖关系。 应该避免这种情况。 另外,当系统需要具有多个不同的记录器时,很难配置这种记录器。 如果我们正在谈论使用Laravel,则另一个缺点是我们没有使用框架提供的功能来解决此问题。
  2. 使用Laravel门面\日志。 通过这种方法,访问该外观的系统部分开始依赖于框架。 在我们不想从框架中删除的系统部分中,这样的解决方案非常合适。 例如,从控制台命令,后台任务,控制器的某些实例编写。 或者,当服务的结构已经很复杂时,将记录器实例扔进去并不是一件容易的事。
  3. 通过app()resolve()框架的助手来解决记录器依赖性。 该方法与使用Facade具有相同的缺点,但是您需要编写更多代码。
  4. 在该记录器将使用的类的构造函数中指定对记录器的依赖性。 同时,应将相同的LoggerInterface指定为类型,以符合DIP要求 。 借助自动装配框架,依赖项将在实现其声明的抽象时自动解决。 在Laravel中,某些依赖项类可以在单独的方法中指定,而不是在整个类的构造函数中指定。

在代码中调用记录器的位置


在项目中组织代码时,可能会出现问题,我应该在哪个类中写入日志。 应该服务吗? 还是应该在从以下位置调用服务的地方完成:控制器,后台任务,控制台命令? 还是每个异常都应该使用其report (Laravel)方法决定要写入日志的report ? 不能一次简单地回答所有问题。


考虑一下Laravel提供的机会 ,将日志记录本身的任务委托给异常类。 异常无法确定系统确定事件级别的严重性。 另外,除非在调用此异常时专门添加了异常,否则该异常无法访问上下文。 要在异常上调用render方法,您必须不捕获该异常(将使用全局ErrorHandler),或捕获并使用report()全局帮助器。 此方法使我们不必每次都能捕获此异常时调用PSR-3记录器。 但是我认为不应该赋予例外这样的责任。


似乎我们总是只能登录服务。 实际上,在某些服务中,您可以进行日志记录。 但是请考虑不依赖于项目的服务,通常,我们计划将其放在单独的程序包中。 然后,该服务不知道其在项目中的重要性,因此将无法确定日志记录的级别。 例如,具有特定SMS网关的集成服务。 如果我们遇到网络错误,那么这并不意味着它很严重。 也许系统具有与另一个SMS网关的集成服务,将通过该服务进行第二次发送,然后可以将来自第一个错误的报告为警告,将第二个错误的报告为error。 只有现在,所有这些集成都应该从另一个服务中调用,该服务将准确登录。 原来,错误是在一项服务中,而我们在另一项中登录。 但是有时候我们没有另一个服务的服务包装器-我们直接从控制器调用它。 在这种情况下,我认为可以写入控制器中的日志,而不是编写用于记录日志的服务装饰器。


显示依赖关系和传递上下文的用法的示例:


 <?php namespace App\Console\Commands; use App\Services\ExampleService; use Illuminate\Console\Command; use Psr\Log\LoggerInterface; class Example extends Command { protected $signature = 'example'; public function handle(ExampleService $service, LoggerInterface $logger) { try { $service->example(); } catch (\Exception $exception) { $logger->critical('Example error', [ 'exception' => $exception, ]); } } } 

在哪里写


考虑以下选项。


  1. 根据12因子应用程序和其他一些建议,您需要在应用程序运行时的stdout,stderr中编写。 为此,您可以在config logger php://stdout指定php://stdout *。
  2. 忽略12因子,docker-way并写入文件。 Laravel(Monolog)甚至允许您配置日志轮转。 可以使用Filebeat收集文件中的其他消息,并将其发送到Logstash进行分析。
  3. 直接从应用程序进一步发送日志,例如通过UDP,以提高性能。
  4. 结合解决方案。 写入将收集使用Filebeat的文件并将其发送到Logstash。 编写容器stderr以便能够使用docker logs命令并准备从容器编排环境方便地收集日志。 在这种情况下,您只能在本地写入某些通道,而某些通道是通过网络发送的。

* 在php-fpm 7.2中,将日志写入stdout时,出现“警告:[pool www]子X表示为stdout ...”,长消息被截断。 解决这个问题的一种方法是在这里 php-fpm 7.3中没有这样的问题。


记录格式选项:


  • 易于阅读(换行符,缩进等)
  • 机器可读(通常是json)
  • 两种格式都可以同时使用:在stdout中机器可读以进行进一步的路由,在出现突然的路由问题和快速调试时可以被人阅读

任何选项都假定日志已路由-至少出于以下原因,它们被发送到单个日志处理系统(存储):


  1. 长期存储和归档
  2. 大规模趋势
  3. 灵活的事件通知系统

Docker可以指定日志管理器。 默认值为json-file ,即docker将容器的输出添加到主机上的json-file。 如果我们选择了一个日志管理器,它将通过网络将记录发送到某个地方,那么我们将无法再使用docker logs 。 如果选择容器的stdout / stderr作为记录应用程序日志的唯一位置,则在网络问题或单个存储库出现问题的情况下,可能无法快速提取记录以进行调试。


我们可以使用json-file docker和Filebeat。 我们将同时收到本地日志和进一步的路由。 值得注意的是,这是泊坞窗的另一个功能。 当记录超过16KB的事件时,泊坞窗使用\n符号将记录中断,这会使许多日志收集器感到困惑。 这有一个问题 。 泊坞窗方面的问题无法解决,因此由收集者解决。 在某些版本中,Filebeat 支持此docker行为并正确组合事件。


您可以自己为项目选择目的地和录制格式的所有可能性的组合。


使用Filebeat + ELK + Elastalert


简而言之,每个服务的作用可以描述如下:


  • Filebeat-从文件收集事件并发送
  • Logstash-解析事件并发送
  • Elasticsearch-存储结构化事件
  • Kibana-显示事件(图形,聚合等)
  • Elastalert-根据请求发送警报

此外,您可以:zabbix,metricbeat,grafana等。


现在更多关于每个。


文件拍


您可以在主机上作为单独的服务运行,也可以使用单独的Docker容器。 要使用来自docker的事件流,它使用主机路径/var/lib/docker/containers/*/*.log 。 Filebeat具有广泛的选项 ,可用于设置各种情况下的行为(重命名文件,删除文件等)。 Filebeat本身可以解析事件内部的json,但json也不能进入事件,这将导致错误。 所有事件处理最好在一个地方完成。


Filebeat 6的片段配置
 filebeat.inputs: - type: docker containers: ids: - "*" processors: - add_docker_metadata: ~ 

Logstash


能够接受来自许多来源的事件,但是这里我们考虑使用Filebeat。
在每个事件中,除了来自stdout / stderr的事件本身之外,还有元数据(主机,容器等)。 有许多内置的处理过滤器:按固定间隔解析,解析json,修改,添加,删除字段等。适用于解析任何格式的应用程序日志和nginx access.log。 能够将数据传输到不同的存储库,但是在这里我们考虑使用Elasticsearch。


Logstash过滤器配置片段
 if [status] { date { match => ["timestamp_nginx_access", "dd/MMM/yyyy:HH:mm:ss Z"] target => "timestamp_nginx" remove_field => ["timestamp_nginx_access"] } mutate { convert => { "bytes_sent" => "integer" "body_bytes_sent" => "integer" "request_length" => "integer" "request_time" => "float" "upstream_response_time" => "float" "upstream_connect_time" => "float" "upstream_header_time" => "float" "status" => "integer" "upstream_status" => "integer" } remove_field => [ "message" ] rename => { "@timestamp" => "event_timestamp" "timestamp_nginx" => "@timestamp" } } } 

弹性搜索


Elasticsearch是执行各种任务的非常强大的工具,但是出于监视日志的目的,Elasticsearch仅知道一定的最小值即可使用。
保存的事件是一个文档,文档存储在索引中。
每个索引都是一个架构,其中为文档的每个字段定义了一种类型。 如果至少一个字段的类型错误,则无法在索引中保存事件。
不同的类型使您可以对一组文档进行不同的操作(对于数字-总和,最小值,最大值,平均值等,对于字符串-模糊搜索等)。
对于日志,管理人员通常建议使用每日索引-每天使用一个新索引。


随着数据量的增长,确保Elasticsearch的稳定运行是一项需要对此工具有深入了解的任务。 但是要快速解决稳定性问题,可以选择自动删除过时的数据。 为此,我建议在logstash中将事件级别拆分为不同的索引。 这将允许更长的时间存储稀有但更重要的事件。


Logstash输出配置代码段
 output { if [fields][log_type] == "app_log" { if [level] in ["DEBUG", "INFO", "NOTICE"] { elasticsearch { hosts => "${ES_HOST}" index => "logstash-app-log-debug-%{+YYYY.MM.dd}" } } else { elasticsearch { hosts => "${ES_HOST}" index => "logstash-app-log-error-%{+YYYY.MM.dd}" } } } } 

要自动删除过时的索引,建议您使用Elastic Curator中的程序。 程序的启动被添加到Cron计划中,配置本身可以存储在单独的文件中。


删除过时索引的配置片段
 action: delete_indices description: logstash-app-log-error options: ignore_empty_list: True filters: - filtertype: pattern kind: prefix value: logstash-app-log-error- - filtertype: age source: name direction: older timestring: '%Y.%m.%d' unit: months unit_count: 6 

, Filebeat Logstash, . Elasticsearch -- , , .


Kibana


Kibana . -, Elasticsearch. .


Kibana — Discovery . , Discovery app warning , time, message, exception class, host, client_id.


, Discovery nginx, 404 time, message, request, status.
Kibana , : , , . , ( ).


图片


Elastalert


Elastalert Elasticsearch . , . , .
, (), .


:


  • ALERT, EMERGENCY. — 10
  • CRITICAL. — 30
  • , N X M
  • 10 INFO 3
  • nginx 200, 201, 304 75% , 50

 name: Blacklist ALERT, EMERGENCY type: blacklist index: logstash-app-* compare_key: "level" blacklist: - "ALERT" - "EMERGENCY" realert: minutes: 5 alert: - "slack" 

. , , . Kibana.


, , http- 75% , , , , . - , , , .


, , , , Kibana, .


5 . , , , , , .


, . .


Kibana . .



docker-. , , staging- production-, .


, Elastalert, . Elastalert ,
envsubst < /opt/elastalert/config.dist.yaml > /opt/elastalert/config.yaml entrypoint- , .


, , , .


Makefile
 build: docker build -t some-registry/elasticsearch elasticsearch docker build -t some-registry/logstash logstash docker build -t some-registry/kibana kibana docker build -t some-registry/nginx nginx docker build -t some-registry/curator curator docker build -t some-registry/elastalert elastalert push: docker push some-registry/elasticsearch docker push some-registry/logstash docker push some-registry/kibana docker push some-registry/nginx docker push some-registry/curator docker push some-registry/elastalert pull: docker pull some-registry/elasticsearch docker pull some-registry/logstash docker pull some-registry/kibana docker pull some-registry/nginx docker pull some-registry/curator docker pull some-registry/elastalert prepare: docker network create -d bridge elk-network || echo "ok" stop: docker rm -f kibana || true docker rm -f logstash || true docker rm -f elasticsearch || true docker rm -f nginx || true docker rm -f elastalert || true run-logstash: docker rm -f logstash || echo "ok" docker run -d --restart=always --network=elk-network --name=logstash -p 127.0.0.1:5001:5001 -e "LS_JAVA_OPTS=-Xms256m -Xmx256m" -e "ES_HOST=elasticsearch:9200" some-registry/logstash run-kibana: docker rm -f kibana || echo "ok" docker run -d --restart=always --network=elk-network --name=kibana -p 127.0.0.1:5601:5601 --mount source=elk-kibana,target=/usr/share/kibana/optimize some-registry/kibana run-elasticsearch: docker rm -f elasticsearch || echo "ok" docker run -d --restart=always --network=elk-network --name=elasticsearch -e "ES_JAVA_OPTS=-Xms1g -Xmx1g" --mount source=elk-esdata,target=/usr/share/elasticsearch/data some-registry/elasticsearch run-nginx: docker rm -f nginx || echo "ok" docker run -d --restart=always --network=elk-network --name=nginx -p 80:80 -v /root/elk/.htpasswd:/etc/nginx/.htpasswd some-registry/nginx run-elastalert: docker rm -f elastalert || echo "ok" docker run -d --restart=always --network=elk-network --name=elastalert --env-file=./elastalert/.env some-registry/elastalert run: prepare run-elasticsearch run-kibana run-logstash run-elastalert delete-old-indices: docker run --rm --network=elk-network -e "ES_HOST=elasticsearch:9200" some-registry/curator curator --config /curator/curator.yml /curator/actions.yml 

:


  • 80 nginx, basic auth Kibana
  • Logstash . ssh-
  • nginx
  • , docker-
  • , .env- nginx-
  • *_JAVA_OPTS , 4GB RAM ( ES).

, xpack-.


docker-compose. , , Dockerfile-, Filebeat, Logstash, , , , , VCS.


. . , ( Laravel scheduler), , 5 . ALERT. , . , , .


结论


, , , . . , - . . , , .

Source: https://habr.com/ru/post/zh-CN456676/


All Articles