AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / server / 问题 / 1017974
Accepted
Michał Paluchowski
Michał Paluchowski
Asked: 2020-05-21 10:12:41 +0800 CST2020-05-21 10:12:41 +0800 CST 2020-05-21 10:12:41 +0800 CST

使用 Amazon Linux 2 在 Elastic Beanstalk 上输出 JSON 日志

  • 772

我们正在尝试将我们的 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 格式输出到标准输出,以便我们以后可以使用 Logs Insights 查询它们。但是有了前缀,Insights 不会“看到” JSON,而只会将整行视为文本 blob。

我在 AWS 上找不到任何文档。几乎所有 Elastic Beanstalk 的文档都提到了 Amazon Linux 的第一个版本。

amazon-web-services elastic-beanstalk amazon-linux-2
  • 4 4 个回答
  • 2956 Views

4 个回答

  • Voted
  1. Best Answer
    Michał Paluchowski
    2020-05-21T23:17:13+08:002020-05-21T23:17:13+08:00

    我找到了一个足够好的解决方案,所以我会在这里发布它以供后代使用。如果有人可以推荐一个更好的,请做。

    Amazon Linux 2 上的 Elastic Beanstalk 依赖于rsyslog日志处理和输出。在部署期间有一个文件/opt/elasticbeanstalk/config/private/rsyslog.conf被复制到/etc/rsyslog.d/web.conf,它将应用程序的所有输出定向web到/var/log/web.stdout.log.

    该文件不包含任何自定义模板。它依赖于rsyslog的默认模板,该模板在 any 前面%msg%加上时间戳和$programname(web在这种情况下)。

    我尝试通过.ebextensionsconfig替换此文件,但这不起作用,因为 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
  2. Mike
    2021-08-07T14:15:25+08:002021-08-07T14:15:25+08:00

    我使用Platform Hooks来实现这一点。唯一的/etc/rsyslog.d/web.conf问题是在应用程序和配置部署上都被替换了,所以你需要一个钩子。

    这种方法避免了弄乱 Elastic Beanstalk 的内部文件/opt/elasticbeanstalk/config/private(自之前的答案以来已更改 -rsyslog.conf不再存在)。此外,平台挂钩现在比 ebextensions 更受欢迎。

    如果您使用 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文件,其中包含每个已定义进程名称的块。因为应用程序和配置部署都可以更改定义的流程,所以在两者之后重新创建此文件是有意义的。

    • 3
  3. puttputt
    2020-06-25T06:36:54+08:002020-06-25T06:36:54+08:00

    我将把我的解决方案放在这里,尽管它与 op 的问题有点不同 - 它围绕着相同的想法,并希望能回答一些其他评论者关于 nodejs 的问题。

    这适用于运行 Node.js 12.x 的 Amazon Linux 2

    问题:nodejs 标准输出日志与“web”下的 nginx 日志混合在一起,并且格式错误。

    以前,在运行 Nodejs 的 Amazon Linux 1 中,这些日志分为/var/log/nodejs/nodejs.log和/var/log/nginx/access.log。将它们组合在一起并以 IP 地址为前缀只会使它们变得一团糟。

    我按照提出的解决方案进行了一些修改。

    1. .ebextension 配置修改 rsyslog.conf 以将日志拆分为两个不同的文件。我正在过滤我在日志文件中看到的模式,但您可以使用任何与 RainerScript 兼容的正则表达式。

    正如另一位评论者指出的那样,我不确定他们是否希望您编辑此文件,因为它是私人的。如果您对此不满意,我建议您写入您自己的日志文件而不是标准输出。这样你就有更多的控制权。

    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. 现在我有一个或多或少的新日志文件,只是节点标准输出,需要流式传输到 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. Anthony Webb
    2021-12-04T11:52:07+08:002021-12-04T11:52:07+08:00

    如果您使用 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);
            }
        });
    };
    
    • 0

相关问题

  • 与 AWS 中的其他系统相比,CentOS 报告的总内存较低

  • 如何在 Amazon Linux 服务器上升级到 Java 1.8?

  • 了解 Amazon AWS 使用数据

  • 亚马逊提供的负载均衡服务体验如何?

  • ELB 中现有节点的 AWS 自动缩放问题

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    新安装后 postgres 的默认超级用户用户名/密码是什么?

    • 5 个回答
  • Marko Smith

    SFTP 使用什么端口?

    • 6 个回答
  • Marko Smith

    命令行列出 Windows Active Directory 组中的用户?

    • 9 个回答
  • Marko Smith

    什么是 Pem 文件,它与其他 OpenSSL 生成的密钥文件格式有何不同?

    • 3 个回答
  • Marko Smith

    如何确定bash变量是否为空?

    • 15 个回答
  • Martin Hope
    Tom Feiner 如何按大小对 du -h 输出进行排序 2009-02-26 05:42:42 +0800 CST
  • Martin Hope
    Noah Goodrich 什么是 Pem 文件,它与其他 OpenSSL 生成的密钥文件格式有何不同? 2009-05-19 18:24:42 +0800 CST
  • Martin Hope
    Brent 如何确定bash变量是否为空? 2009-05-13 09:54:48 +0800 CST
  • Martin Hope
    cletus 您如何找到在 Windows 中打开文件的进程? 2009-05-01 16:47:16 +0800 CST

热门标签

linux nginx windows networking ubuntu domain-name-system amazon-web-services active-directory apache-2.4 ssh

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve