本文将讨论日志记录的好处。 我将介绍有关PSR的日志。 我将添加一些有关使用已记录事件的级别,消息和上下文的个人建议。 将给出一个示例,说明如何在用Laravel编写并通过Docker在多个实例上启动的应用程序中使用ELK组织日志记录和监视。 我将签署警告系统的重要规则。 我将给出一个脚本示例,该脚本使用一个命令来引发整个监视堆栈。
日志记录的好处
井井有条的日志记录至少允许以下内容:
- 要知道有些事情没有按预期进行(有错误)
- 了解错误的详细信息,这将有助于说明错误发生的原因和对象,并防止重复发生
- 要知道一切都按计划进行(access.log,debug级,info级)
日志本身并不能告诉您所有这一切,但是借助日志,可以独立地查找事件的详细信息,或者为日志配置监视系统,以便能够通知问题。 如果日志中的消息带有足够数量的上下文,则这将大大简化调试,因为您将可以访问有关事件发生情况的更多数据。
写什么和写什么
php社区的一部分为一些代码编写任务提出了建议。 一种这样的建议是PSR-3 Logger Interface 。 它仅描述您需要记录的内容。 为此,开发了“ Psr\Log\LoggerInterface
/ log”软件包的Psr\Log\LoggerInterface
。 使用它时,您需要了解事件的三个组成部分:
- 级别 -事件重要性
- 消息 -描述事件的文字
- 上下文 -有关事件的其他信息数组
PSR-3事件级别
这些级别是从RFC 5424-Syslog协议中借用的,其大致描述如下:
- debug-调试详细信息
- 信息-有趣的事件
- 注意-重大事件,但非错误
- 警告-例外情况,但没有错误
- 错误-不需要瞬时干预的执行错误
- 严重-严重情况(系统组件不可用,意外异常)
- 警报-采取行动需要立即干预
- 紧急-系统无法运作
有描述,但由于难以确定某些事件的重要性,因此并不总是易于理解。 例如,在单个请求的上下文中,不可能访问连接的资源。 在记录此事件时,我们不知道一个请求是否失败,或者只有一个用户失败。 这取决于是否需要立即进行干预,或者这是否是罕见的情况,它可以等待甚至被忽略。 在监视日志的框架中解决了此类问题。 但是您仍然需要确定级别。 因此,可以商定团队中的日志记录级别。 一个例子:
- 紧急情况是外部系统的级别,可以查看您的系统并确定它完全不起作用,或者其自我诊断不起作用。
- 警报 -系统本身可以例如通过计划的任务来诊断其状态,并以此级别记录事件。 它可以是已连接资源的检查,也可以是特定的检查,例如,所用外部资源帐户的余额。
- 严重的 -发生故障时,如果发生故障的系统组件非常重要且应始终有效,则该事件很重要。 它已经很大程度上取决于系统的功能。 即使只发生一次,也适用于对快速发现很重要的事件。
- 错误 -发生了一个事件,有关该事件,如果很快重复,您需要报告。 无法完成必须执行的操作,但是此操作不属于关键说明。 例如,无法根据用户的请求保存个人资料图片,但是该系统不是个人资料图片服务,而是聊天系统。
- 警告 -事件,要立即通知您,您需要在一段时间内拨打大量事件。 无法执行某项操作,该操作的失败不会破坏任何严重的问题。 这些仍然是错误,但可能需要等待工作时间表进行更正。 例如,不可能保存用户的头像,并且该系统是在线商店。 为了了解突发异常,必须(频繁)通知它们,因为它们可能是更严重问题的症状。
- 注意 -这些事件报告系统提供的偏差,这些偏差是系统正常运行的一部分。 例如,用户在入口处输入了错误的密码,用户没有填写中间名,但这不是必须的,用户购买了0卢布的订单,但这在极少数情况下为您提供。 还需要高频率地通知它们,因为偏差数量的急剧增加可能是紧急需要修复的错误的结果。
- info-事件,事件的发生报告了系统的正常运行。 例如,用户已注册,用户已购买产品,用户已留下反馈。 此类事件的通知需要以相反的方式进行配置:如果一段时间内发生的此类事件数量不足,则需要进行通知,因为这些错误的减少可能是由于错误导致的。
- debug-用于调试系统上进程的事件。 当您向事件的上下文中添加足够的数据时,您可以诊断问题,或得出该过程在系统中正常运行的结论。 例如,用户打开了一个产品页面并收到了推荐列表。 显着增加了发送的事件数,因此允许在一段时间后删除此类事件的日志记录。 结果,在正常操作中此类事件的数量将是可变的,因此可以省略对它们的通知监视。
活动讯息
通过PSR-3,消息必须是字符串或带有__toString()
方法的对象。 此外,根据PSR-3,消息行可能包含”User {username} created”
形式的占位符,可以用上下文数组中的值替换这些占位符。 当使用Elasticsearch和Kibana进行监视时,我建议不要使用占位符,而应该写固定的行,因为这将简化事件的过滤,并且上下文将始终存在。 另外,我建议注意此消息的其他要求:
- 案文应简短但有意义。 这是警报中将要出现的内容,并且将在已发生的事件列表中显示。
- 对于程序的不同部分,文本最好是唯一的。 这将使警报可以在不查看上下文的情况下了解事件发生在哪个部分。
事件背景
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, ]
从代码调用记录器
为了在日志中快速记录一个事件,您可以提供几个选项。 让我们考虑其中的一些。
- 声明全局函数
log()
并从程序的不同部分调用它。 这种方法有许多缺点。 例如,在我们访问该函数的类中,形成了隐式依赖关系。 应该避免这种情况。 另外,当系统需要具有多个不同的记录器时,很难配置这种记录器。 如果我们正在谈论使用Laravel,则另一个缺点是我们没有使用框架提供的功能来解决此问题。 - 使用Laravel门面\日志。 通过这种方法,访问该外观的系统部分开始依赖于框架。 在我们不想从框架中删除的系统部分中,这样的解决方案非常合适。 例如,从控制台命令,后台任务,控制器的某些实例编写。 或者,当服务的结构已经很复杂时,将记录器实例扔进去并不是一件容易的事。
- 通过
app()
和resolve()
框架的助手来解决记录器依赖性。 该方法与使用Facade具有相同的缺点,但是您需要编写更多代码。 - 在该记录器将使用的类的构造函数中指定对记录器的依赖性。 同时,应将相同的
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, ]); } } }
在哪里写
考虑以下选项。
- 根据12因子应用程序和其他一些建议,您需要在应用程序运行时的stdout,stderr中编写。 为此,您可以在config logger
php://stdout
指定php://stdout
*。 - 忽略12因子,docker-way并写入文件。 Laravel(Monolog)甚至允许您配置日志轮转。 可以使用Filebeat收集文件中的其他消息,并将其发送到Logstash进行分析。
- 直接从应用程序进一步发送日志,例如通过UDP,以提高性能。
- 结合解决方案。 写入将收集使用Filebeat的文件并将其发送到Logstash。 编写容器stderr以便能够使用
docker logs
命令并准备从容器编排环境方便地收集日志。 在这种情况下,您只能在本地写入某些通道,而某些通道是通过网络发送的。
* 在php-fpm 7.2中,将日志写入stdout时,出现“警告:[pool www]子X表示为stdout ...”,长消息被截断。 解决这个问题的一种方法是在这里 。 php-fpm 7.3中没有这样的问题。
记录格式选项:
- 易于阅读(换行符,缩进等)
- 机器可读(通常是json)
- 两种格式都可以同时使用:在stdout中机器可读以进行进一步的路由,在出现突然的路由问题和快速调试时可以被人阅读
任何选项都假定日志已路由-至少出于以下原因,它们被发送到单个日志处理系统(存储):
- 长期存储和归档
- 大规模趋势
- 灵活的事件通知系统
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. , . , , .
结论
, , , . . , - . . , , .