我们正在尝试将 Java 应用程序从当前的 Elastic Beanstalk JDK 8 平台迁移到在 Amazon Linux 2 上运行 Corretto 11 的新平台。该应用程序运行良好,但日志处理方式发生了变化。现在,Web 进程的输出存储在 中/var/log/web.stdout.log
,每行都以时间戳和进程名称为前缀,即:
May 20 17:00:00 ip-10-48-41-129 web: {"timestamp":"2020-05-20T17:00:00.035Z","message":"...","logger":"...","thread":"MessageBroker-2","level":"INFO"}
我们如何才能去掉前缀?这些日志被流式传输到 CloudWatch,我们将它们以 JSON 格式输出到 stdout,以便我们稍后可以使用 Logs Insights 查询它们。但是使用前缀,Insights 不会“看到”JSON,而只是将整行视为文本 blob。
我在 AWS 上找不到任何相关文档。几乎所有 Elastic Beanstalk 文档都涉及 Amazon Linux 的第一个版本。
答案1
我用了平台挂钩来实现这一点。唯一的问题是,/etc/rsyslog.d/web.conf
应用程序和配置部署都会被替换,因此您需要一个钩子来同时处理这两者。
这种方法避免了干扰 Elastic Beanstalk 的内部文件/opt/elasticbeanstalk/config/private
(自上次回答以来,这些文件已发生更改 -rsyslog.conf
不再存在)。此外,现在平台挂钩比 ebextensions 更受欢迎。
如果您使用 CodeBuild,请不要忘记在输出工件中包含platformFiles
目录(或放置文件的任何位置)。
笔记:此代码假定进程名称为web
。如果您在 中定义了不同的进程名称Procfile
,请使用该名称。但是,我思考/etc/rsyslog.d/web.conf
无论进程名称如何,rsyslog 配置都应该始终存在。
确保所有.sh
文件都可以使用 执行chmod +x
。
.platform/hooks/predeploy/10_logs.sh
#!/bin/sh
sudo platformFiles/setupLogs.sh
.platform/confighooks/predeploy/10_logs.sh
#!/bin/sh
sudo platformFiles/setupLogs.sh
platformFiles/setupLogs.sh
#!/bin/sh
# By default logs output to /var/log/web.stdout.log are prefixed. We want just the raw logs from the app.
# This updates the rsyslog config. Also grants read permissions to the log files.
set -eu
mv platformFiles/rsyslogWebConf.conf /etc/rsyslog.d/web.conf
touch /var/log/web.stdout.log
touch /var/log/web.stderr.log
chmod +r /var/log/web.stdout.log
chmod +r /var/log/web.stderr.log
systemctl restart rsyslog.service
platformFiles/rsyslogWebConf.conf
# This file is created from Elastic Beanstalk platform hooks.
template(name="WebTemplate" type="string" string="%msg%\n")
if $programname == 'web' then {
*.=warning;*.=err;*.=crit;*.=alert;*.=emerg; /var/log/web.stderr.log;WebTemplate
*.=info;*.=notice /var/log/web.stdout.log;WebTemplate
}
推测
看起来已/opt/elasticbeanstalk/config/private/rsyslog.conf
被替换为/opt/elasticbeanstalk/config/private/rsyslog.conf.template
:
# This rsyslog file redirects Elastic Beanstalk platform logs.
# Logs are initially sent to syslog, but we also want to divide
# stdout and stderr into separate log files.
{{range .ProcessNames}}if $programname == '{{.}}' then {
*.=warning;*.=err;*.=crit;*.=alert;*.=emerg /var/log/{{.}}.stderr.log
*.=info;*.=notice /var/log/{{.}}.stdout.log
}
{{end}}
基于此,我推测 Elastic Beanstalk 使用此模板生成一个/etc/rsyslog.d/web.conf
文件,其中包含每个已定义流程名称的块。由于应用程序和配置部署都可以更改已定义的流程,因此在两者之后重新创建此文件是有意义的。
答案2
我找到了一个效果很好的解决方案,所以我会在这里发布以供后人参考。如果有人能提出更好的建议,请提出来。
Amazon Linux 2 上的 Elastic Beanstalk 依赖于进行日志处理和输出。部署期间会将 复制到 处的rsyslog
一个文件,该文件会将应用程序的所有输出定向到 处。/opt/elasticbeanstalk/config/private/rsyslog.conf
/etc/rsyslog.d/web.conf
web
/var/log/web.stdout.log
该文件不包含任何自定义模板。它依赖于rsyslog
的默认模板,该模板在任何 前面都加上%msg%
时间戳和前缀$programname
(在本例中为web
)。
我尝试通过替换此文件.ebextensions
配置,但这不起作用,因为 Elastic Beanstalk 似乎在.ebextensions
运行后覆盖了这个文件。所以我添加了一个附加平台挂钩删除该文件,保留我添加的自定义文件。
这是.ebextensions/logs.config
文件:
files:
"/etc/rsyslog.d/web-files.conf":
mode: "000644"
owner: root
group: root
content: |
template(name="WebTemplate" type="string" string="%msg%\n")
if $programname == 'web' then {
*.=warning;*.=err;*.=crit;*.=alert;*.=emerg; /var/log/web.stderr.log;WebTemplate
*.=info;*.=notice /var/log/web.stdout.log;WebTemplate
}
commands:
remove-.bak-rsyslog:
command: rm -f *.bak
cwd: /etc/rsyslog.d
并且.platform/hooks/predeploy/remove-default-rsyslog-conf.sh
(请确保你chmod +x
这一个):
#!/bin/sh
rm /etc/rsyslog.d/web.conf
systemctl restart rsyslog.service
答案3
我将在这里提出我的解决方案,尽管它与 op 的问题略有不同 - 但它围绕着相同的想法,并希望能回答其他评论者关于 nodejs 的一些问题。
这是为了运行 Node.js 12.x 的 Amazon Linux 2
问题:nodejs stdout 日志与“web”下的 nginx 日志混合在一起,并且格式很糟糕。
以前,在运行 Nodejs 的 Amazon Linux 1 中,这些日志被分成/var/log/nodejs/nodejs.log
和/var/log/nginx/access.log
。将它们合并并加上 ip 地址前缀只会让它们变得一团糟。
我遵循了所提出的解决方案并对其进行了一些修改。
- .ebextension 配置可修改 rsyslog.conf,将日志拆分为两个不同的文件。我正在根据日志文件中看到的模式进行过滤,但您可以使用任何与 RainerScript 兼容的正则表达式。
我不确定他们是否希望你编辑这个文件,正如另一位评论者指出的那样,因为它是私密的。如果你对此不满意,我建议你写入自己的日志文件而不是 stdout。这样你就可以拥有更多的控制权。
files:
"/opt/elasticbeanstalk/config/private/rsyslog.conf" :
mode: "000755"
owner: root
group: root
content: |
template(name="WebTemplate" type="string" string="%msg%\n")
if $programname == 'web' and $msg startswith '#033[' then {
*.=warning;*.=err;*.=crit;*.=alert;*.=emerg; /var/log/web.stderr.log
*.=info;*.=notice /var/log/web.stderr.log;
} else if( $programname == 'web') then /var/log/node/nodejs.log;WebTemplate
- 现在我有了一个新的日志文件,它或多或少只是节点 stdout,需要将其流式传输到 cloudwatch 并包含在日志包中。这些配置已记录(尽管未针对 Amazon Linux 2 进行更新)。这里没有覆盖任何配置,它们只是添加新配置。
files:
"/opt/elasticbeanstalk/config/private/logtasks/bundle/node.conf" :
mode: "000755"
owner: root
group: root
content: |
/var/log/node/nodejs.log
files:
"/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/node.json" :
mode: "000755"
owner: root
group: root
content: |
{
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/log/node/nodejs.log",
"log_group_name": "/aws/elasticbeanstalk/[environment_name]/var/log/node/nodejs.log",
"log_stream_name": "{instance_id}"
}
]
}
}
}
}
commands:
append_and_restart:
command: /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a append-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/node.json -s
答案4
如果您使用 lambda 将内容移动到 loggly 中(根据此处的文档)https://documentation.solarwinds.com/en/success_center/loggly/content/admin/cloudwatch-logs.htm)您可以简单地修改 lambda 以提取 JSON 并删除导致问题的前导字符串。这是我正在使用的 lambda,它可以很好地清理内容并发布我的 JSON 日志。
附注:我使用 NodeJS - Fastify,它包含 Pino,用于生成漂亮的 JSON 日志。有关设置日志的一些精彩见解可在此处找到https://jaywolfe.dev/blog/setup-your-fastify-server-with-logging-the-right-way-no-more-express/
'use strict';
/*
* To encrypt your secrets use the following steps:
*
* 1. Create or use an existing KMS Key - http://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html
*
* 2. Expand "Encryption configuration" and click the "Enable helpers for encryption in transit" checkbox
*
* 3. Paste <your loggly customer token> into the kmsEncryptedCustomerToken environment variable and click "Encrypt"
*
* 4. Give your function's role permission for the `kms:Decrypt` action using the provided policy template
*/
const AWS = require('aws-sdk');
const http = require('http');
const zlib = require('zlib');
// loggly url, token and tag configuration
// user needs to edit environment variables when creating function via blueprint
// logglyHostName, e.g. logs-01.loggly.com
// logglyTags, e.g. CloudWatch2Loggly
const logglyConfiguration = {
hostName: process.env.logglyHostName,
tags: process.env.logglyTags,
};
// use KMS to decrypt customer token in kmsEncryptedCustomerToken environment variable
const decryptParams = {
CiphertextBlob: Buffer.from(process.env.kmsEncryptedCustomerToken, 'base64'),
EncryptionContext: { LambdaFunctionName: process.env.AWS_LAMBDA_FUNCTION_NAME },
};
const kms = new AWS.KMS({ apiVersion: '2014-11-01' });
kms.decrypt(decryptParams, (error, data) => {
if (error) {
logglyConfiguration.tokenInitError = error;
console.log(error);
} else {
logglyConfiguration.customerToken = data.Plaintext.toString('ascii');
}
});
// entry point
exports.handler = (event, context, callback) => {
const payload = Buffer.from(event.awslogs.data, 'base64');
// converts the event to a valid JSON object with the sufficient infomation required
function parseEvent(logEvent, logGroupName, logStreamName) {
return {
// remove '\n' character at the end of the event
message: extractJSON(logEvent.message.trim())[0],
logGroupName,
logStreamName,
timestamp: new Date(logEvent.timestamp).toISOString(),
};
}
// joins all the events to a single event
// and sends to Loggly using bulk endpoint
function postEventsToLoggly(parsedEvents) {
if (!logglyConfiguration.customerToken) {
if (logglyConfiguration.tokenInitError) {
console.log('error in decrypt the token. Not retrying.');
return callback(logglyConfiguration.tokenInitError);
}
console.log('Cannot flush logs since authentication token has not been initialized yet. Trying again in 100 ms.');
setTimeout(() => postEventsToLoggly(parsedEvents), 100);
return;
}
// get all the events, stringify them and join them
// with the new line character which can be sent to Loggly
// via bulk endpoint
const finalEvent = parsedEvents.map(JSON.stringify).join('\n');
// creating logglyURL at runtime, so that user can change the tag or customer token in the go
// by modifying the current script
// create request options to send logs
try {
const options = {
hostname: logglyConfiguration.hostName,
path: `/bulk/${logglyConfiguration.customerToken}/tag/${encodeURIComponent(logglyConfiguration.tags)}`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': finalEvent.length,
},
};
const req = http.request(options, (res) => {
res.on('data', (data) => {
const result = JSON.parse(data.toString());
if (result.response === 'ok') {
callback(null, 'all events are sent to Loggly');
} else {
console.log(result.response);
}
});
res.on('end', () => {
console.log('No more data in response.');
callback();
});
});
req.on('error', (err) => {
console.log('problem with request:', err.toString());
callback(err);
});
// write data to request body
req.write(finalEvent);
req.end();
} catch (ex) {
console.log(ex.message);
callback(ex.message);
}
}
function extractJSON(str) {
var firstOpen, firstClose, candidate;
firstOpen = str.indexOf('{', firstOpen + 1);
do {
firstClose = str.lastIndexOf('}');
console.log('firstOpen: ' + firstOpen, 'firstClose: ' + firstClose);
if(firstClose <= firstOpen) {
return null;
}
do {
candidate = str.substring(firstOpen, firstClose + 1);
console.log('candidate: ' + candidate);
try {
var res = JSON.parse(candidate);
console.log('...found');
return [res, firstOpen, firstClose + 1];
}
catch(e) {
console.log('...failed');
}
firstClose = str.substr(0, firstClose).lastIndexOf('}');
} while(firstClose > firstOpen);
firstOpen = str.indexOf('{', firstOpen + 1);
} while(firstOpen != -1);
}
zlib.gunzip(payload, (error, result) => {
if (error) {
callback(error);
} else {
const resultParsed = JSON.parse(result.toString('ascii'));
const parsedEvents = resultParsed.logEvents.map((logEvent) =>
parseEvent(logEvent, resultParsed.logGroup, resultParsed.logStream));
postEventsToLoggly(parsedEvents);
}
});
};