CloudWatch Logsを介さずにLambdaのテレメトリを行うnewrelic-lambda-extensionとその仕組み

newrelicinfraawslog

New RelicにLambdaのログを転送するには、CloudWatch Logsに出力したものをサブスクライブして送るLambda function aws-log-ingestionを用いる従来の方法のほかに、Lambda layer newrelic-lambda-extensionを用いる方法があって、トレースログなどをCloudWatc Logsに出力することなく送れるのでコストを最小限に抑えられる。

インストール

newrelic-lambda integrations install するとLayerが参照するAPI KeyのSecretのStackなどがデプロイされる。

$ pip3 install newrelic-lambda-cli
$ newrelic-lambda integrations install \
    --nr-account-id <account id> \
    --nr-api-key <api key> \
    --linked-account-name <linked account name> \
    --enable-license-key-secret \
    --aws-profile <aws_profile_name>
    --aws-region <aws_region>
Validating New Relic credentials
Retrieving integration license key
Creating the AWS role for the New Relic AWS Lambda Integration
Waiting for stack creation to complete... ✔️ Done
✔️ Created role [NewRelicLambdaIntegrationRole_*****] in AWS account.
Linking New Relic account to AWS account
✔️ Cloud integrations account [*****] already exists in New Relic account [*****] with IAM role [arn:aws:iam::*****:role/newrelic].
Enabling Lambda integration on the link between New Relic and AWS
✔️ Cloud integrations account [*****] is using CloudWatch Metric Streams
Creating the managed secret for the New Relic License Key
Setting up NewRelicLicenseKeySecret stack in region: us-east-1
Creating change set: NewRelicLicenseKeySecret-CREATE-1648894216
Waiting for change set creation to complete, this may take a minute... Waiting for change set to finish execution. This may take a minute... ✔️ Done
Creating newrelic-log-ingestion Lambda function in AWS account
Setting up 'newrelic-log-ingestion' function in region: us-east-1
Fetching new CloudFormation template url
Creating change set: NewRelicLogIngestion-CREATE-1648894271
Waiting for change set creation to complete, this may take a minute... Waiting for change set to finish execution. This may take a minute... ✖️ Failed to create 'newrelic-log-ingestion' function: 'Status'
✖️ Install Incomplete. See messages above for details.

このStackがExportしているArnのRead policyを与えてLayerと環境変数を設定する。処理を追いやすいようにデバッグログを出すようにしている。

$ cat template.yaml
...
Resources:
  TestNewRelicFunction:
    Type: AWS::Serverless::Function 
    Properties:
      Runtime: go1.x
      Handler: main
      CodeUri: hello-world/
      Environment:  
        Variables:
          NEW_RELIC_ACCOUNT_ID: *****
          NEW_RELIC_TRUSTED_ACCOUNT_KEY: *****
          NEW_RELIC_EXTENSION_SEND_FUNCTION_LOGS: true
          NEW_RELIC_EXTENSION_LOG_LEVEL: DEBUG
      Layers:
        - !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:451483290750:layer:NewRelicLambdaExtension:12
      Policies:
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: !ImportValue NewRelicLicenseKeySecret-NewRelic-LicenseKeySecretARN

lambda.Start(handler)nrlambda.Start(handler, app) に置き換えると他のintegrationと同様にContextにTransactionが書き込まれる。 logrus integrationでログにtrace.idを付加している。

New Relicでインフラやアプリケーションをモニタリングする - sambaiz-net

package main

import (
	"context"
	"fmt"

	"github.com/newrelic/go-agent/v3/integrations/logcontext/nrlogrusplugin"
	"github.com/newrelic/go-agent/v3/integrations/nrlambda"
	"github.com/newrelic/go-agent/v3/newrelic"
	"github.com/sirupsen/logrus"
)

func handler(ctx context.Context) (interface{}, error) {
	logger := logrus.New()
	logger.SetFormatter(nrlogrusplugin.ContextFormatter{})
	logger.WithContext(ctx).Info("test")

	return "ok", nil
}

func main() {
	app, err := newrelic.NewApplication(
		nrlambda.ConfigOption(),
	)
	if nil != err {
		fmt.Println("error creating app (invalid config):", err)
	}
	nrlambda.Start(handler, app)
}

これを実行すると次のようなログがCloudWatch Logsに出力され、New Relicの画面上でトレースとログを確認できる。

START RequestId: 4a11de7f-c798-4cb4-bcb9-089482d0bec0 Version: $LATEST
[NR_EXT] New Relic Lambda Extension starting up
[NR_EXT] Registration response: {"functionName":"newrelic-test-app-TestNewRelicFunction-BhEz8V8OMy0F","functionVersion":"$LATEST","handler":"main"}
[NR_EXT] Log registration with request  {"buffering":{"maxBytes":1048576,"maxItems":10000,"timeoutMs":500},"destination":{"URI":"http://sandbox:45533","protocol":"HTTP"},"types":["platform","function"]}
[NR_EXT] Starting log server.
LOGS	Name: newrelic-lambda-extension	State: Subscribed	Types: [platform,function]
[NR_EXT] Registered for logs. Got response code  200 "OK"
EXTENSION	Name: newrelic-lambda-extension	State: Ready	Events: [SHUTDOWN,INVOKE]
[NR_EXT] Agent telemetry bytes: *****
{"entity.type":"SERVICE","hostname":"169.254.184.29","timestamp":1649389879807,"log.level":"info","trace.id":"809a2ae4f456626900d9252ba39901ce","span.id":"5bac32a65bcb1df0","message":"test"}
[NR_EXT] Sent 1/1 New Relic payload batches with 1 log events successfully in 184.426ms (164ms to transmit 1.1kB).
END RequestId: 4a11de7f-c798-4cb4-bcb9-089482d0bec0
REPORT RequestId: 4a11de7f-c798-4cb4-bcb9-089482d0bec0	Duration: 260.07 ms	Billed Duration: 261 ms	Memory Size: 128 MB	Max Memory Used: 56 MB	Init Duration: 287.13 ms

仕組み

newrelic-lambda-extension は関数ログとテレメトリ情報を異なる方法で取って送っている。

SendFunctionLogs()

環境変数NEW_RELIC_EXTENSION_SEND_FUNCTION_LOGStrueのとき関数ログを送る。 newrelic-lambda-extensionはlog serverを起動し、Lambdaの拡張機能のランタイムログAPIによってsandbox.localdomainに送られるログをハンドリングして関数ログは送り、起動などのプラットフォームログはテレメトリ情報に付加する

ランタイムログAPIのエンドポイントは環境変数AWS_LAMBDA_RUNTIME_APIで取得できて、PUT /logsを呼ぶとサブスクライブできる。

SendTelemetry()

トレースデータを含むテレメトリ情報を送る。こちらはmkfifo()システムコールで作成したpipeで待ち受け、agentはこのpipeがある場合stdoutの代わりにpipeに書き込むようになっている。

Unixのパイプをmkfifo()で作ってdup2()で標準出力にコピーして書き込む - sambaiz-net