Вывод журналов JSON на Elastic Beanstalk с Amazon Linux 2

Мы пытаемся перенести наши 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.

5
задан 20 May 2020 в 21:12
4 ответа

Я нашел решение, которое работает достаточно хорошо, так что я отправлю это здесь для потомков. Если кто-то может предложить лучший вариант, сделайте это.

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
3
ответ дан 4 January 2021 в 07:28

Я собираюсь изложить здесь свое решение, хотя это немного другая проблема, чем 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-адресом просто превращает их в абсолютный беспорядок.

Я следил за решениями, которые были перенаправлены, и немного изменил их.

  1. .ebextension config, который изменяет rsyslog.conf для разделения журналов на два разных файла . Я фильтрую по шаблону, который видел в своих файлах журнала, но вы можете использовать любое регулярное выражение, совместимое с RainerScript.

Я не уверен, что они хотят, чтобы вы редактировали этот файл, как указал другой комментатор , 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

  1. Теперь, когда у меня есть новый файл журнала, который более или менее представляет собой простой узел 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
0
ответ дан 4 January 2021 в 07:28

Для этого я использовал Крюки платформы.Единственная загвоздка в том, что /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, который содержит блок для каждого определенного имени процесса.Поскольку и приложение, и развертывание конфигурации могут изменить определенные процессы, имеет смысл воссоздать этот файл после обоих.

2
ответ дан 6 August 2021 в 22:15

Если вы используете лямбду для перемещения данных в 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);
        }
    });
};
0
ответ дан 3 December 2021 в 18:52

Теги

Похожие вопросы