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

newrelicaws

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が書き込まれる。 現状、テレメトリデータが送られないとlambdaのタイムアウトまで待ち続けるようになっているので置き換えは必須。

次の例では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_LOGS がtrueのとき関数ログを送る。 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