Мы пытаемся перенести наши Java-приложения с текущих платформ Elastic Beanstalk JDK 8 на новые, на которых работает Corretto 11 на Amazon Linux 2. The приложение работает хорошо, но способ обработки журналов изменился. Выходные данные веб-процесса теперь хранятся в /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"}
Как мы можем избавиться от prefix? Эти журналы передаются в CloudWatch, и мы выводим их на стандартный вывод в формате JSON, чтобы позже можно было запросить их с помощью Logs Insights. Но с префиксом Insights не «видит» JSON и просто обрабатывает всю строку как текстовый blob.
Я не могу найти для этого документацию на AWS. Почти вся документация по Elastic Beanstalk относится к первой версии Amazon Linux.
Я нашел решение, которое работает достаточно хорошо, так что я отправлю это здесь для потомков. Если кто-то может предложить лучший вариант, сделайте это.
Elastic Beanstalk в Amazon Linux 2 полагается на 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 / Preploy / remove-default-rsyslog-conf.sh
(убедитесь, что вы chmod + x
этот):
#!/bin/sh
rm /etc/rsyslog.d/web.conf
systemctl restart rsyslog.service
Я собираюсь изложить здесь свое решение, хотя это немного другая проблема, чем op - это примерно та же идея и, надеюсь, ответит на некоторые другие вопросы комментаторов о nodejs.
Это для Amazon Linux 2 под управлением Node.js 12.x
Проблема : журналы stdout nodejs смешаны с журналами nginx в разделе «Интернет» и плохо отформатированы.
Ранее в В Amazon Linux 1 под управлением Nodejs эти журналы разделены на /var/log/nodejs/nodejs.log
и /var/log/nginx/access.log
. Их объединение и префикс с IP-адресом просто превращает их в абсолютный беспорядок.
Я следил за решениями, которые были перенаправлены, и немного изменил их.
Я не уверен, что они хотят, чтобы вы редактировали этот файл, как указал другой комментатор , Sincei является личным. Если вам это не нравится, я бы порекомендовал писать в свои собственные файлы журнала, а не в стандартный вывод. Таким образом, у вас будет больше контроля.
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
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
Для этого я использовал Крюки платформы.Единственная загвоздка в том, что /etc/rsyslog.d/web.conf
заменяется как при развертывании приложения, так и при развертывании конфигурации, поэтому вам нужен хук для обоих.
Этот подход позволяет избежать проблем с внутренними файлами Elastic Beanstalk в /opt/elasticbeanstalk/config/private
(которые изменились по сравнению с предыдущими ответами — rsyslog.conf
больше не существует). Кроме того, крюки на платформе теперь предпочтительнее, чем удлинители.
Если вы используете CodeBuild, не забудьте включить каталог platformFiles
(или куда бы вы ни поместили свои файлы) в выходной артефакт.
ПРИМЕЧАНИЕ. Этот код предполагает, что имя процесса — web
. Если вы определили другое имя процесса в вашем Procfile
, используйте его. Однако я думаю, что конфигурация rsyslog всегда должна быть в /etc/rsyslog.d/web.conf
, несмотря на имя процесса.
Убедитесь, что все ваши файлы .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
, который содержит блок для каждого определенного имени процесса.Поскольку и приложение, и развертывание конфигурации могут изменить определенные процессы, имеет смысл воссоздать этот файл после обоих.
Если вы используете лямбду для перемещения данных в loggly (в соответствии с их документацией здесь https://documentation.solarwinds.com/en/success_center/loggly/content/admin/cloudwatch-logs.htm), вы можете просто изменить лямбду, чтобы извлечь JSON и удалить начальную строку, которая вызывает горе. Вот лямбда, которую я использую, и она отлично работает для очистки и публикации моих журналов 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);
}
});
};