diff --git a/.gitignore b/.gitignore index 744e13e6..a59b9b08 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ extensions/ response.out .aws-sam/ coverage.txt -preview-extensions-ggqizro707 +extenstion.zip +.DS_Store diff --git a/Makefile b/Makefile index 5650f3de..c3d1690b 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,32 @@ build: clean go build -o ./extensions/newrelic-lambda-extension +build-arm64: clean + mkdir extensions + env GOARCH=arm64 GOOS=linux go build -ldflags="-s -w" -o ./extensions/newrelic-lambda-extension + chmod +x ./extensions/newrelic-lambda-extension + zip -r ./extensions/extension.zip ./extensions/ + +build-x86_64: clean + mkdir extensions + env GOARCH=amd64 GOOS=linux go build -ldflags="-s -w" -o ./extensions/newrelic-lambda-extension + chmod +x ./extensions/newrelic-lambda-extension + zip -r ./extensions/extension.zip ./extensions/ + clean: rm -rf extensions - rm -f preview-extensions-ggqizro707 - rm -f /tmp/newrelic-lambda-extension.x86_64.zip - rm -f /tmp/newrelic-lambda-extension.arm64.zip -dist-x86_64: clean +ci-build-x86_64: clean env GOARCH=amd64 GOOS=linux go build -ldflags="-s -w" -o ./extensions/newrelic-lambda-extension - touch preview-extensions-ggqizro707 -dist-arm64: clean +ci-build-arm64: clean env GOARCH=arm64 GOOS=linux go build -ldflags="-s -w" -o ./extensions/newrelic-lambda-extension - touch preview-extensions-ggqizro707 -zip-x86_64: dist-x86_64 - zip -r /tmp/newrelic-lambda-extension.x86_64.zip preview-extensions-ggqizro707 extensions +zip-x86_64: ci-build-x86_64 + zip -r /tmp/newrelic-lambda-extension.x86_64.zip extensions zip-arm64: dist-arm64 - zip -r /tmp/newrelic-lambda-extension.arm64.zip preview-extensions-ggqizro707 extensions + zip -r /tmp/newrelic-lambda-extension.arm64.zip extensions test: @echo "Normal tests" @@ -29,6 +36,3 @@ test: coverage: ./coverage.sh - -publish: zip-x86_64 - aws lambda publish-layer-version --no-cli-pager --layer-name newrelic-lambda-extension-x86_64 --zip-file fileb:///tmp/newrelic-lambda-extension.x86_64.zip diff --git a/README.md b/README.md index cf3b7fff..a3f69751 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,18 @@ -[![Community Project header](https://github.com/newrelic/opensource-website/raw/master/src/images/categories/Community_Project.png)](https://opensource.newrelic.com/oss-category/#community-project) - -# newrelic-lambda-extension [![Build Status](https://circleci.com/gh/newrelic/newrelic-lambda-extension.svg?style=svg)](https://circleci.com/gh/newrelic/newrelic-lambda-extension) [![Coverage](https://codecov.io/gh/newrelic/newrelic-lambda-extension/branch/main/graph/badge.svg?token=T73UEDVA5K)](https://codecov.io/gh/newrelic/newrelic-lambda-extension) - -An AWS Lambda extension to collect, enhance, and transport telemetry data from your AWS Lambda functions to New Relic without requiring an external transport such as CloudWatch Logs or Kinesis. - -This lightweight AWS Lambda Extension runs alongside your AWS Lambda functions and automatically handles the collection and transport of telemetry data from -supported New Relic serverless agents. +# New Relic Telemetry API Extension + +The New Relic Telemetry API Extension collects telemetry data from both AWS Lambda Telemetry API and New Relic Agents and sends it to New Relic. Please note that Telemetry API collects all logs for an invocation, so when using an agent do not forward logs or they will be duplicated. If you are using a New Relic Agent, be sure to follow its [lambda/serverless installation documentation](https://docs.newrelic.com/docs/serverless-function-monitoring/aws-lambda-monitoring/enable-lambda-monitoring/instrument-example/). The use of this layer does not require a New Relic agent to be present, and can be configured to send telemetry without one. The following environment variables are available to configure this extension: + +| Environment Variable | Required | Description | +| --- | --- | --- | +| NEW_RELIC_ACCOUNT_ID | **always** | The account ID of the New Relic account you want to send data to | +| NEW_RELIC_LICENSE_KEY | *conditional* | A plaintext New Relic license key. Either this or the value retrieved from NEW_RELIC_LICENSE_KEY_SECRET must contain a valid New Relic license key or the application will exit. If a plaintext license key is provided, it will override the license key retrieved from an AWS Secret. | +| NEW_RELIC_LICENSE_KEY_SECRET | *conditional* | The name of an AWS Secrets Manager Secret containing a New Relic license key. If no plaintext license key is provided, the value of this variable must be set to the name of an AWS Secret containing a valid New Relic license key. | +| NEW_RELIC_EXTENSION_AGENT_DATA_COLLECTION_ENABLED | optional | Setting this to "false" will prevent the extension from collecting data from New Relic Agents. This will not prevent the agent from running. | +| NEW_RELIC_EXTENSION_AGENT_DATA_BATCH_SIZE | optional | The number of invocations to store before sending Agent Data to New Relic. If your lamba function gets invoked at a high frequency, increasing this number will improve the performance of the extension and avoid dropped data and improve performance. Default: 1 | +| NEW_RELIC_EXTENSION_TELEMETRY_API_BATCH_SIZE | optional | The number of Telemetry API events and logs to batch before sending them to New Relic. If your application invokes frequently, increase this number to avoid data getting dropped and to improve performance. Default: 1 | +| NEW_RELIC_EXTENSION_DATA_COLLECTION_TIMEOUT | optional | A valid time.Duration string for how long the extension should wait to attempt to send agent data to New Relic in the event of a timeout/retry loop scenario. Example: 1s, 1500ms; Default: 10s | +| NEW_RELIC_EXTENSION_COLLECTOR_OVERRIDE | optional | An override for the New Relic collection endpoint you want to send data to. By default, this will be detected based on the region of your New Relic license key. | +| NEW_RELIC_EXTENSION_LOG_LEVEL | optional | The log level of the New Relic Telemetry API Extension. For more verbose logs, set to "debug". To log only warnings and errors, set it to "warn". For Advanced troubleshooting, set to "trace". Note that debug, and especially trace, log levels will cause a significant increase in log lines that are printed and saved to cloudwatch. | ## Installation @@ -16,118 +23,9 @@ Lambda function. The current layer ARN can be found [here][3]. **Note:** This extension is included with all New Relic AWS Lambda layers going forward. -You'll also need to make the New Relic license key available to the extension. Use the [New Relic Lambda CLI][4] -to install the managed secret, and then add the permission for the secret to your Lambda execution role. - -[4]: https://github.com/newrelic/newrelic-lambda-cli - - newrelic-lambda integrations install \ - --nr-account-id \ - --nr-api-key \ - --linked-account-name \ - --enable-license-key-secret - -Each of the example functions in the `examples` directory has the appropriate license key secret permission. - -After deploying your AWS Lambda function with one of the layer ARNs from the -link above you should begin seeing telemetry data in New Relic. - -See below for details on supported New Relic agents. - -## Supported Configurations - -AWS's [Extension API supports](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-extensions-api.html) only a subset -of all their runtimes. Notably absent as of this writing are Node JS before 10, Python before 3.7, Go (all versions), -Dotnet before 3.1, and the older "java8" runtime, though "java8.al2" is supported. - -For Go lambdas, we suggest using "provided" or "provided.al2". The Go example's deploy script contains compiler flags -that produce a suitable self-hosting Go executable. See the [Custom runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) -docs for more details on this feature. - -All of our layers include the extension, and the latest Agent version for the Layer's runtime. The latest -layer version ARNs for your runtime and region are available [here](https://layers.newrelic-external.com/). The -`NewRelicLambdaExtension` layer is suitable for Go, Java and Dotnet. - -## Building - -Use the included `Makefile` to compile the extension. - -```sh -make dist -``` - -This creates the extension binary in `./extensions/newrelic-lambda-extension`. The binary is compiled for Amazon Linux, which is likely different from the platform you're working on. - -## Deploying - -To publish the extension to your AWS account, run the following command: - -```sh - make publish -``` - -This packages the extension, and publishes a new layer version in your AWS account. Be sure that the AWS CLI is configured correctly. You can use the usual [AWS CLI environment variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html) to control the account and region for the CLI. - -## Startup Checks - -This Lambda Extension will perform a series of checks on initialization. Should any of -these checks fail, the extension wil attempt to output troubleshooting recommendations to both -CloudWatch Logs and New Relic Logs. If you have any issues using this extension, be sure -to check your logs for messages starting with `Startup check failed:` for -troubleshooting recommendations. - -Startup checks include: - -* New Relic agent version checks -* Lambda handler configuration checks -* Lambda environment variable checks -* Vendored New Relic agent checks - -## Disabling Extension - -The New Relic Lambda Extension is enabled by default. To disable it, after adding or -updating the Lambda layer, set the `NEW_RELIC_LAMBDA_EXTENSION_ENABLED` environment -variable to `false`. - -## Testing - -To test locally, acquire the AWS extension test harness first. Then: - ->TODO: Link to the AWS SDK that has the test harness, assuming it gets published. - -1. (Optional) Use the `newrelic-lambda` CLI to create the license key managed secret in your AWS account and region. -2. Build the docker container for sample function code. Give it the tag `lambda_ext`. - - Be sure to include your lambda function in the container. -3. Start up your container. - - - Using AWS Secret Manager - - export AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id --profile default) - export AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key --profile default) - export AWS_SESSION_TOKEN=$(aws configure get aws_session_token --profile default) - - docker run --rm -v $(pwd)/extensions:/opt/extensions -p 9001:8080 \ - -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \ - lambda_ext:latest \ - -h function.handler -c '{}' -t 60000 - - - Or, setting the license key directly - - docker run --rm \ - -v $(pwd)/extensions:/opt/extensions \ - -p 9001:8080 \ - lambda_ext:latest \ - -h function.handler -c '{"NEW_RELIC_LICENSE_KEY": "your-license-key-here"}' -t 60000 - -4. To invoke the sample lambda run: - - curl -XPOST 'http://localhost:9001/2015-03-31/functions/function.handler/invocations' \ - -d 'invoke-payload' - -5. Finally, you can exercise the container shutdown lifecycle event with: +## Building Locally - curl -XPOST 'http://localhost:9001/test/shutdown' \ - -d '{"timeoutMs": 5000 }' +Use the `make build-arm64` or `make build-amd64` commands to build local version of this binary. Make sure that you have docker enabled and conifgured properly. This runs the build command in a linux docker container to prevent errors caused by MacOS filesystem artifacts from occuring. ## Support diff --git a/agentTelemetry/batch.go b/agentTelemetry/batch.go new file mode 100644 index 00000000..03e87f63 --- /dev/null +++ b/agentTelemetry/batch.go @@ -0,0 +1,121 @@ +package agentTelemetry + +import ( + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + MustHarvestError = "telemetry can not be added until batch is harvested" +) + +var l = log.WithFields(log.Fields{"pkg": "agentTelemetry"}) + +// Batch represents the unsent invocations and their telemetry, along with timing data. +type Batch struct { + extractTraceID bool + harvestSize int + invocations map[string]*Invocation +} + +// NewBatch constructs a new batch. +func NewBatch(size int, extractTraceID bool, logLevel log.Level) *Batch { + log.SetLevel(logLevel) + return &Batch{ + invocations: make(map[string]*Invocation), + harvestSize: size, + extractTraceID: extractTraceID, + } +} + +func (b *Batch) ReadyToHarvest() bool { + l.Debugf("[agentTelemetry] Have %d / %d invocations needed to do a harvest", len(b.invocations), b.harvestSize) + return len(b.invocations) >= b.harvestSize +} + +// AddInvocation should be called just after the next API response. It creates the Invocation record so that we can attach telemetry later. +func (b *Batch) AddInvocation(requestID string, start time.Time) { + invocation := NewInvocation(requestID, start) + b.invocations[requestID] = &invocation +} + +// HasInvocation checks if an invocation has been created for this request ID +func (b *Batch) HasInvocation(requestID string) bool { + _, ok := b.invocations[requestID] + return ok +} + +// AddTelemetry attaches telemetry to an existing Invocation, identified by requestId +func (b *Batch) AddTelemetry(requestId string, telemetry []byte) *Invocation { + inv, ok := b.invocations[requestId] + if ok { + inv.Telemetry = append(inv.Telemetry, telemetry) + if b.extractTraceID { + traceId, err := ExtractTraceID(telemetry) + if err != nil { + l.Errorf("[agent telemtry: ExtractTraceID] %v", err) + } + // We don't want to unset a previously set trace ID + if traceId != "" { + inv.TraceId = traceId + } + } + return inv + } + return nil +} + +// Close aggressively harvests all telemetry from the Batch. The Batch is no longer valid. +func (b *Batch) Close() []*Invocation { + return b.Harvest(true) +} + +// Harvest all ready invocations. It removes harvested invocations from the batch and updates the lastHarvest timestamp. +func (b *Batch) Harvest(force bool) []*Invocation { + ret := make([]*Invocation, 0, len(b.invocations)) + for k, v := range b.invocations { + if force { + if !v.IsEmpty() { + ret = append(ret, v) + delete(b.invocations, k) + } + } else { + if !v.IsEmpty() && v.IsRipe() { + ret = append(ret, v) + delete(b.invocations, k) + } + } + } + + l.Debugf("[agentTelemetry] harvesting %d invocations\n", len(ret)) + return ret +} + +// An Invocation holds telemetry for a request, and knows when the request began. +// Invocations are parts of a Batch, and should only be used by the batch object. +type Invocation struct { + Start time.Time + RequestId string + TraceId string + Telemetry [][]byte +} + +// NewInvocation creates an Invocation, which can hold telemetry +func NewInvocation(requestId string, start time.Time) Invocation { + return Invocation{ + Start: start, + RequestId: requestId, + Telemetry: make([][]byte, 0, 2), + } +} + +// IsRipe indicates that an Invocation has all the telemetry it's likely to get. Sending a ripe invocation won't omit data. +func (inv *Invocation) IsRipe() bool { + return len(inv.Telemetry) >= 1 +} + +// IsEmpty is true when the invocation has no telemetry. The invocation has begun, but has received no agent payload, nor platform logs. +func (inv *Invocation) IsEmpty() bool { + return len(inv.Telemetry) == 0 +} diff --git a/agentTelemetry/batch_test.go b/agentTelemetry/batch_test.go new file mode 100644 index 00000000..a7b9639d --- /dev/null +++ b/agentTelemetry/batch_test.go @@ -0,0 +1,162 @@ +package agentTelemetry + +import ( + "bytes" + "testing" + "time" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +const ( + testTelemetry = "test_telemetry" + moreTestTelemetry = "more_test_telemetry" + testRequestId = "test_a" + testRequestId2 = "test_b" + testRequestId3 = "test_c" + testNoSuchRequestId = "test_z" +) + +var DefaultBatchSize = 1 + +func TestMissingInvocation(t *testing.T) { + batch := NewBatch(DefaultBatchSize, false, log.InfoLevel) + + invocation := batch.AddTelemetry(testNoSuchRequestId, bytes.NewBufferString(testTelemetry).Bytes()) + assert.Nil(t, invocation) +} + +func TestEmptyHarvestForce(t *testing.T) { + batch := NewBatch(DefaultBatchSize, false, log.InfoLevel) + res := batch.Harvest(true) + + assert.Equal(t, 0, len(res)) +} + +func TestEmptyHarvest(t *testing.T) { + batch := NewBatch(DefaultBatchSize, false, log.InfoLevel) + res := batch.Harvest(false) + + assert.Equal(t, 0, len(res)) +} + +func TestFullHarvest(t *testing.T) { + batch := NewBatch(DefaultBatchSize, false, log.InfoLevel) + requestStart := time.Now() + + batch.AddInvocation(testRequestId, requestStart) + batch.AddInvocation(testRequestId2, requestStart.Add(100*time.Millisecond)) + batch.AddInvocation(testRequestId3, requestStart.Add(200*time.Millisecond)) + + invocation := batch.AddTelemetry(testRequestId, bytes.NewBufferString(testTelemetry).Bytes()) + assert.NotNil(t, invocation) + + invocation2 := batch.AddTelemetry(testRequestId, bytes.NewBufferString(moreTestTelemetry).Bytes()) + assert.Equal(t, invocation, invocation2) + + batch.AddTelemetry(testRequestId2, bytes.NewBufferString(testTelemetry).Bytes()) + + harvested := batch.Harvest(false) + + // 2/3 of invocations have data + assert.Equal(t, 2, len(harvested)) + + // all harvested invocations should have at least 1 payload + // and harvests without any payloads should not be harvested + for _, harvest := range harvested { + assert.NotEqual(t, testRequestId3, harvest.RequestId) + assert.GreaterOrEqual(t, len(harvest.Telemetry), 1) + } +} + +func TestHarvestWithTraceID(t *testing.T) { + batch := NewBatch(DefaultBatchSize, true, log.InfoLevel) + requestStart := time.Now() + + batch.AddInvocation(testRequestId, requestStart) + batch.AddInvocation(testRequestId2, requestStart.Add(100*time.Millisecond)) + batch.AddInvocation(testRequestId3, requestStart.Add(200*time.Millisecond)) + + invocation := batch.AddTelemetry(testRequestId, bytes.NewBufferString(testTelemetry).Bytes()) + assert.NotNil(t, invocation) + + invocation2 := batch.AddTelemetry(testRequestId, bytes.NewBufferString(moreTestTelemetry).Bytes()) + assert.Equal(t, invocation, invocation2) + + batch.AddTelemetry(testRequestId2, bytes.NewBufferString(testTelemetry).Bytes()) + + harvested := batch.Harvest(false) + assert.Equal(t, 2, len(harvested)) + + for _, harvest := range harvested { + assert.GreaterOrEqual(t, len(harvest.Telemetry), 1) + } +} + +func TestNotFullHarvest(t *testing.T) { + batch := NewBatch(DefaultBatchSize, false, log.InfoLevel) + requestStart := time.Now() + + batch.AddInvocation(testRequestId, requestStart) + batch.AddInvocation(testRequestId2, requestStart.Add(100*time.Millisecond)) + + invocation := batch.AddTelemetry(testRequestId, bytes.NewBufferString(testTelemetry).Bytes()) + assert.NotNil(t, invocation) + + invocation2 := batch.AddTelemetry(testRequestId, bytes.NewBufferString(moreTestTelemetry).Bytes()) + assert.Equal(t, invocation, invocation2) + + batch.AddTelemetry(testRequestId2, bytes.NewBufferString(testTelemetry).Bytes()) + + // This should not get harvested + batch.AddInvocation(testRequestId3, requestStart.Add(300*time.Millisecond)) + + harvested := []*Invocation{} + if batch.ReadyToHarvest() { + harvested = batch.Harvest(false) + } + + assert.Equal(t, 2, len(harvested)) + assert.NotEqual(t, testRequestId3, harvested[0].RequestId) + assert.NotEqual(t, testRequestId3, harvested[1].RequestId) +} + +func TestForcedHarvest(t *testing.T) { + batch := NewBatch(DefaultBatchSize, false, log.InfoLevel) + requestStart := time.Now() + + batch.AddInvocation(testRequestId, requestStart) + batch.AddInvocation(testRequestId2, requestStart.Add(100*time.Millisecond)) + + invocation := batch.AddTelemetry(testRequestId, bytes.NewBufferString(testTelemetry).Bytes()) + assert.NotNil(t, invocation) + + invocation2 := batch.AddTelemetry(testRequestId, bytes.NewBufferString(moreTestTelemetry).Bytes()) + assert.Equal(t, invocation, invocation2) + + batch.AddTelemetry(testRequestId2, bytes.NewBufferString(testTelemetry).Bytes()) + + harvested := batch.Harvest(true) + assert.Equal(t, 2, len(harvested)) +} + +func TestBatch_Close(t *testing.T) { + batch := NewBatch(DefaultBatchSize, false, log.InfoLevel) + requestStart := time.Now() + + batch.AddInvocation(testRequestId, requestStart) + batch.AddInvocation(testRequestId2, requestStart.Add(100*time.Millisecond)) + batch.AddInvocation(testRequestId3, requestStart.Add(200*time.Millisecond)) + + invocation := batch.AddTelemetry(testRequestId, bytes.NewBufferString(testTelemetry).Bytes()) + assert.NotNil(t, invocation) + + invocation2 := batch.AddTelemetry(testRequestId, bytes.NewBufferString(moreTestTelemetry).Bytes()) + assert.Equal(t, invocation, invocation2) + + batch.AddTelemetry(testRequestId2, bytes.NewBufferString(testTelemetry).Bytes()) + + harvested := batch.Close() + assert.Equal(t, 2, len(harvested)) +} diff --git a/telemetry/client.go b/agentTelemetry/client.go similarity index 55% rename from telemetry/client.go rename to agentTelemetry/client.go index 5a4415c4..59c01a33 100644 --- a/telemetry/client.go +++ b/agentTelemetry/client.go @@ -1,11 +1,11 @@ -package telemetry +package agentTelemetry import ( "bytes" "context" "encoding/binary" "fmt" - "io/ioutil" + "io" "log" "net" "net/http" @@ -15,16 +15,13 @@ import ( crypto_rand "crypto/rand" math_rand "math/rand" - "github.com/newrelic/newrelic-lambda-extension/lambda/logserver" - - "github.com/newrelic/newrelic-lambda-extension/util" + "newrelic-lambda-extension/config" + "newrelic-lambda-extension/util" ) const ( InfraEndpointEU string = "https://cloud-collector.eu01.nr-data.net/aws/lambda/v1" InfraEndpointUS string = "https://cloud-collector.newrelic.com/aws/lambda/v1" - LogEndpointEU string = "https://log-api.eu.newrelic.com/log/v1" - LogEndpointUS string = "https://log-api.newrelic.com/log/v1" // WIP (configuration options?) SendTimeoutRetryBase time.Duration = 200 * time.Millisecond @@ -35,18 +32,18 @@ const ( type Client struct { httpClient *http.Client batch *Batch + compressTool *util.CompressTool timeout time.Duration licenseKey string telemetryEndpoint string - logEndpoint string functionName string collectTraceID bool } // New creates a telemetry client with sensible defaults -func New(functionName string, licenseKey string, telemetryEndpointOverride string, logEndpointOverride string, batch *Batch, collectTraceID bool, clientTimeout time.Duration) *Client { +func New(conf config.Config, batch *Batch, collectTraceID bool) *Client { httpClient := &http.Client{ - Timeout: 2400 * time.Millisecond, + Timeout: util.SendToNewRelicTimeout, } // Create random seed for timeout to avoid instances created at the same time @@ -54,24 +51,23 @@ func New(functionName string, licenseKey string, telemetryEndpointOverride strin var b [8]byte _, err := crypto_rand.Read(b[:]) if err != nil { - log.Fatal("cannot seed math/rand package with cryptographically secure random number generator") + log.Fatal("[New Client] cannot seed math/rand package with cryptographically secure random number generator") } math_rand.Seed(int64(binary.LittleEndian.Uint64(b[:]))) - return NewWithHTTPClient(httpClient, functionName, licenseKey, telemetryEndpointOverride, logEndpointOverride, batch, collectTraceID, clientTimeout) + return NewWithHTTPClient(httpClient, conf.ExtensionName, conf.LicenseKey, conf.AgentTelemetryRegion, batch, collectTraceID, conf.DataCollectionTimeout) } // NewWithHTTPClient is just like New, but the HTTP client can be overridden -func NewWithHTTPClient(httpClient *http.Client, functionName string, licenseKey string, telemetryEndpointOverride string, logEndpointOverride string, batch *Batch, collectTraceID bool, clientTimeout time.Duration) *Client { +func NewWithHTTPClient(httpClient *http.Client, functionName string, licenseKey string, telemetryEndpointOverride string, batch *Batch, collectTraceID bool, clientTimeout time.Duration) *Client { telemetryEndpoint := getInfraEndpointURL(licenseKey, telemetryEndpointOverride) - logEndpoint := getLogEndpointURL(licenseKey, logEndpointOverride) return &Client{ httpClient: httpClient, licenseKey: licenseKey, telemetryEndpoint: telemetryEndpoint, - logEndpoint: logEndpoint, functionName: functionName, batch: batch, + compressTool: util.NewCompressTool(), collectTraceID: collectTraceID, timeout: clientTimeout, } @@ -90,20 +86,8 @@ func getInfraEndpointURL(licenseKey string, telemetryEndpointOverride string) st return InfraEndpointUS } -// getLogEndpointURL returns the Vortex endpoint for the provided license key -func getLogEndpointURL(licenseKey string, logEndpointOverride string) string { - if logEndpointOverride != "" { - return logEndpointOverride - } - - if strings.HasPrefix(licenseKey, "eu") { - return LogEndpointEU - } - - return LogEndpointUS -} - -func (c *Client) SendTelemetry(ctx context.Context, invokedFunctionARN string, telemetry [][]byte) (error, int) { +// SendTelemetry attempts to send telemetry data to new relic and returns an error and a succesful payload count +func (c *Client) SendTelemetry(ctx context.Context, invokedFunctionARN string, telemetry [][]byte) (int, error) { start := time.Now() logEvents := make([]LogsEvent, 0, len(telemetry)) for _, payload := range telemetry { @@ -111,9 +95,9 @@ func (c *Client) SendTelemetry(ctx context.Context, invokedFunctionARN string, t logEvents = append(logEvents, logEvent) } - compressedPayloads, err := CompressedPayloadsForLogEvents(logEvents, c.functionName, invokedFunctionARN) + compressedPayloads, err := CompressedPayloadsForLogEvents(c.compressTool, logEvents, c.functionName, invokedFunctionARN) if err != nil { - return err, 0 + return 0, err } var builder requestBuilder = func(buffer *bytes.Buffer) (*http.Request, error) { @@ -125,17 +109,16 @@ func (c *Client) SendTelemetry(ctx context.Context, invokedFunctionARN string, t end := time.Now() totalTime := end.Sub(start) transmissionTime := end.Sub(transmitStart) - util.Logf( - "Sent %d/%d New Relic payload batches with %d log events successfully in %.3fms (%dms to transmit %.1fkB).\n", + l.Infof( + "[agentTelemetry:sendTelemetry] Sent %d/%d New Relic payload batches successfully in %.3fms (%dms to transmit %.1fkB)", successCount, len(compressedPayloads), - len(telemetry), float64(totalTime.Microseconds())/1000.0, transmissionTime.Milliseconds(), float64(sentBytes)/1024.0, ) - return nil, successCount + return successCount, nil } type requestBuilder func(buffer *bytes.Buffer) (*http.Request, error) @@ -157,17 +140,17 @@ func (c *Client) sendPayloads(compressedPayloads []*bytes.Buffer, builder reques select { case <-timer.C: - response.Error = fmt.Errorf("failed to send data within user defined timeout period: %s", c.timeout.String()) + response.Error = fmt.Errorf("[agentTelemetry:sendPayloads] failed to send data within user defined timeout period: %s", c.timeout.String()) quit <- true case response = <-data: timer.Stop() } if response.Error != nil { - util.Logf("Telemetry client error: %s", response.Error) + l.Warnf("[agentTelemetry:sendPayloads] Telemetry client error: %s", response.Error) sentBytes -= p.Len() } else if response.Response.StatusCode >= 300 { - util.Logf("Telemetry client response: [%s] %s", response.Response.Status, response.ResponseBody) + l.Warnf("[agentTelemetry:sendPayloads] Telemetry client response: [%s] %s", response.Response.Status, response.ResponseBody) } else { successCount += 1 } @@ -206,7 +189,7 @@ func (c *Client) attemptSend(currentPayloadBytes []byte, builder requestBuilder, // Success. Process response and exit retry loop defer util.Close(res.Body) - bodyBytes, err := ioutil.ReadAll(res.Body) + bodyBytes, err := io.ReadAll(res.Body) if err != nil { dataChan <- AttemptData{ Error: err, @@ -225,7 +208,7 @@ func (c *Client) attemptSend(currentPayloadBytes []byte, builder requestBuilder, // if error is http timeout, retry if err, ok := err.(net.Error); ok && err.Timeout() { - util.Debugln("Retrying after timeout", err) + l.Debug("[attemptSend] Retrying after timeout", err) time.Sleep(baseSleepTime + time.Duration(math_rand.Intn(400))) // double wait time after 3 timed out attempts @@ -245,63 +228,3 @@ func (c *Client) attemptSend(currentPayloadBytes []byte, builder requestBuilder, } } } - -func (c *Client) SendFunctionLogs(ctx context.Context, invokedFunctionARN string, lines []logserver.LogLine) error { - start := time.Now() - - common := map[string]interface{}{ - "plugin": util.Id, - "faas.arn": invokedFunctionARN, - "faas.name": c.functionName, - } - - logMessages := make([]FunctionLogMessage, 0, len(lines)) - for _, l := range lines { - // Unix time in ms - ts := l.Time.UnixNano() / 1e6 - var traceId string - if c.batch != nil && c.collectTraceID { - // There is a race condition here. Telemetry batch may be late, so the trace - // ID would be blank. This would require a lock to handle, which would delay - // logs being sent. Not sure if worth the performance hit yet. - traceId = c.batch.RetrieveTraceID(l.RequestID) - } - logMessages = append(logMessages, NewFunctionLogMessage(ts, l.RequestID, traceId, string(l.Content))) - util.Debugf("Sending function logs for request %s", l.RequestID) - } - // The Log API expects an array - logData := []DetailedFunctionLog{NewDetailedFunctionLog(common, logMessages)} - - // Since the Log API won't send us more than 1MB, we shouldn't have any issues with payload size. - compressedPayload, err := CompressedJsonPayload(logData) - if err != nil { - return err - } - compressedPayloads := []*bytes.Buffer{compressedPayload} - - var builder requestBuilder = func(buffer *bytes.Buffer) (*http.Request, error) { - req, err := BuildVortexRequest(ctx, c.logEndpoint, buffer, util.Name, c.licenseKey) - if err != nil { - return nil, err - } - - req.Header.Add("X-Event-Source", "logs") - return req, err - } - - transmitStart := time.Now() - successCount, sentBytes := c.sendPayloads(compressedPayloads, builder) - end := time.Now() - totalTime := end.Sub(start) - transmissionTime := end.Sub(transmitStart) - util.Logf( - "Sent %d/%d New Relic function log batches successfully in %.3fms (%dms to transmit %.1fkB).\n", - successCount, - len(compressedPayloads), - float64(totalTime.Microseconds())/1000.0, - transmissionTime.Milliseconds(), - float64(sentBytes)/1024.0, - ) - - return nil -} diff --git a/telemetry/client_test.go b/agentTelemetry/client_test.go similarity index 78% rename from telemetry/client_test.go rename to agentTelemetry/client_test.go index d303feaf..366591e8 100644 --- a/telemetry/client_test.go +++ b/agentTelemetry/client_test.go @@ -1,16 +1,18 @@ -package telemetry +package agentTelemetry import ( "context" "encoding/json" - "io/ioutil" + "io" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" - "github.com/newrelic/newrelic-lambda-extension/util" + "newrelic-lambda-extension/config" + "newrelic-lambda-extension/util" + "github.com/stretchr/testify/assert" ) @@ -27,9 +29,9 @@ func TestClientSend(t *testing.T) { assert.Equal(t, r.Header.Get("User-Agent"), "newrelic-lambda-extension") assert.Equal(t, r.Header.Get("X-License-Key"), "a mock license key") - reqBytes, err := ioutil.ReadAll(r.Body) + reqBytes, err := io.ReadAll(r.Body) assert.NoError(t, err) - defer util.Close(r.Body) + defer Close(r.Body) assert.NotEmpty(t, reqBytes) reqBody, err := util.Uncompress(reqBytes) @@ -46,16 +48,23 @@ func TestClientSend(t *testing.T) { defer srv.Close() - client := NewWithHTTPClient(srv.Client(), "", "a mock license key", srv.URL, srv.URL, &Batch{}, false, clientTestingTimeout) + client := NewWithHTTPClient(srv.Client(), "", "a mock license key", srv.URL, &Batch{}, false, clientTestingTimeout) ctx := context.Background() bytes := []byte("foobar") - err, successCount := client.SendTelemetry(ctx, "arn:aws:lambda:us-east-1:1234:function:newrelic-example-go", [][]byte{bytes}) + successCount, err := client.SendTelemetry(ctx, "arn:aws:lambda:us-east-1:1234:function:newrelic-example-go", [][]byte{bytes}) assert.NoError(t, err) assert.Equal(t, 1, successCount) - client = New("", "mock license key", srv.URL, srv.URL, &Batch{}, false, clientTestingTimeout) + conf := config.Config{ + DataCollectionTimeout: clientTestingTimeout, + ExtensionName: "", + LicenseKey: "mock license key", + AgentTelemetryRegion: srv.URL, + } + + client = New(conf, &Batch{}, false) assert.NotNil(t, client) } @@ -74,9 +83,9 @@ func TestClientSendRetry(t *testing.T) { assert.Equal(t, r.Header.Get("User-Agent"), "newrelic-lambda-extension") assert.Equal(t, r.Header.Get("X-License-Key"), "a mock license key") - reqBytes, err := ioutil.ReadAll(r.Body) + reqBytes, err := io.ReadAll(r.Body) assert.NoError(t, err) - defer util.Close(r.Body) + defer Close(r.Body) assert.NotEmpty(t, reqBytes) reqBody, err := util.Uncompress(reqBytes) @@ -97,11 +106,12 @@ func TestClientSendRetry(t *testing.T) { httpClient := srv.Client() httpClient.Timeout = 50 * time.Millisecond - client := NewWithHTTPClient(httpClient, "", "a mock license key", srv.URL, srv.URL, &Batch{}, false, clientTestingTimeout) + + client := NewWithHTTPClient(httpClient, "", "a mock license key", srv.URL, &Batch{}, false, clientTestingTimeout) ctx := context.Background() bytes := []byte("foobar") - err, successCount := client.SendTelemetry(ctx, "arn:aws:lambda:us-east-1:1234:function:newrelic-example-go", [][]byte{bytes}) + successCount, err := client.SendTelemetry(ctx, "arn:aws:lambda:us-east-1:1234:function:newrelic-example-go", [][]byte{bytes}) assert.NoError(t, err) assert.Equal(t, 1, successCount) @@ -118,11 +128,11 @@ func TestClientReachesDataTimeout(t *testing.T) { httpClient := srv.Client() httpClient.Timeout = 100 * time.Millisecond - client := NewWithHTTPClient(httpClient, "", "a mock license key", srv.URL, srv.URL, &Batch{}, false, clientTestingTimeout) + client := NewWithHTTPClient(httpClient, "", "a mock license key", srv.URL, &Batch{}, false, clientTestingTimeout) ctx := context.Background() bytes := []byte("foobar") - err, successCount := client.SendTelemetry(ctx, "arn:aws:lambda:us-east-1:1234:function:newrelic-example-go", [][]byte{bytes}) + successCount, err := client.SendTelemetry(ctx, "arn:aws:lambda:us-east-1:1234:function:newrelic-example-go", [][]byte{bytes}) assert.LessOrEqual(t, int(time.Since(startTime)), int(clientTestingTimeout+250*time.Millisecond)) assert.NoError(t, err) assert.Equal(t, 0, successCount) @@ -133,11 +143,11 @@ func TestClientUnreachableEndpoint(t *testing.T) { Timeout: time.Millisecond * 1, } - client := NewWithHTTPClient(httpClient, "", "a mock license key", "http://10.123.123.123:12345", "http://10.123.123.123:12345", &Batch{}, false, clientTestingTimeout) + client := NewWithHTTPClient(httpClient, "", "a mock license key", "http://10.123.123.123:12345", &Batch{}, false, clientTestingTimeout) ctx := context.Background() bytes := []byte("foobar") - err, successCount := client.SendTelemetry(ctx, "arn:aws:lambda:us-east-1:1234:function:newrelic-example-go", [][]byte{bytes}) + successCount, err := client.SendTelemetry(ctx, "arn:aws:lambda:us-east-1:1234:function:newrelic-example-go", [][]byte{bytes}) assert.Nil(t, err) assert.Equal(t, 0, successCount) @@ -153,11 +163,11 @@ func TestClientGetsHTTPError(t *testing.T) { httpClient := srv.Client() httpClient.Timeout = 100 * time.Millisecond - client := NewWithHTTPClient(httpClient, "", "a mock license key", srv.URL, srv.URL, &Batch{}, false, clientTestingTimeout) + client := NewWithHTTPClient(httpClient, "", "a mock license key", srv.URL, &Batch{}, false, clientTestingTimeout) ctx := context.Background() bytes := []byte("foobar") - err, successCount := client.SendTelemetry(ctx, "arn:aws:lambda:us-east-1:1234:function:newrelic-example-go", [][]byte{bytes}) + successCount, err := client.SendTelemetry(ctx, "arn:aws:lambda:us-east-1:1234:function:newrelic-example-go", [][]byte{bytes}) assert.Less(t, int(time.Since(startTime)), int(clientTestingTimeout)) // should exit as soon as a non-timeout error occurs without retrying assert.NoError(t, err) assert.Equal(t, 0, successCount) @@ -168,9 +178,3 @@ func TestGetInfraEndpointURL(t *testing.T) { assert.Equal(t, InfraEndpointUS, getInfraEndpointURL("us license key", "")) assert.Equal(t, InfraEndpointEU, getInfraEndpointURL("eu license key", "")) } - -func TestGetLogEndpointURL(t *testing.T) { - assert.Equal(t, "barbaz", getLogEndpointURL("foobar", "barbaz")) - assert.Equal(t, LogEndpointUS, getLogEndpointURL("us mock license key", "")) - assert.Equal(t, LogEndpointEU, getLogEndpointURL("eu mock license key", "")) -} diff --git a/agentTelemetry/close.go b/agentTelemetry/close.go new file mode 100644 index 00000000..b4e09a26 --- /dev/null +++ b/agentTelemetry/close.go @@ -0,0 +1,13 @@ +package agentTelemetry + +import ( + "io" +) + +// Close closes things and logs errors if it fails +func Close(thing io.Closer) { + err := thing.Close() + if err != nil { + l.Errorf("[agentTelemetry]: %v", err) + } +} diff --git a/agentTelemetry/dispatcher.go b/agentTelemetry/dispatcher.go new file mode 100644 index 00000000..9cde16ce --- /dev/null +++ b/agentTelemetry/dispatcher.go @@ -0,0 +1,83 @@ +package agentTelemetry + +import ( + "context" + "encoding/base64" + "newrelic-lambda-extension/config" + "newrelic-lambda-extension/extensionApi" + "time" +) + +type AgentTelemetryDispatcher struct { + collectData bool + telemetryChan chan []byte + batch *Batch + telemetryClient *Client +} + +// NewDispatcher creates a new dispatcher object to manage the collection and sending of New Relic Agent data +func NewDispatcher(conf config.Config) *AgentTelemetryDispatcher { + batch := NewBatch(conf.AgentTelemetryBatchSize, false, conf.LogLevel) + telemetryClient := New(conf, batch, true) + telemetryChan, err := InitTelemetryChannel() + if err != nil { + l.Fatalf("[agentTelemetry] agent telemetry dispatcher failed to create telemetry channel: %v", err) + } + + l.Tracef("[agentTelemtry] client: %+v", telemetryClient) + + return &AgentTelemetryDispatcher{ + collectData: conf.CollectAgentData, + telemetryChan: telemetryChan, + telemetryClient: telemetryClient, + batch: batch, + } +} + +// Dispatch collects agent data and attempts to send it if appropriate +// If force = true, collect and send data no matter what +func (disp *AgentTelemetryDispatcher) Dispatch(ctx context.Context, res *extensionApi.NextEventResponse, force bool) { + // Fetch and Batch latest agent telemetry if possible + select { + case telemetryBytes := <-disp.telemetryChan: + if !disp.collectData { + return + } + + l.Tracef("[agentTelemetry] Agent telemetry bytes: %s", base64.URLEncoding.EncodeToString(telemetryBytes)) + if !disp.batch.HasInvocation(res.RequestID) { + disp.batch.AddInvocation(res.RequestID, time.Now()) + } + disp.batch.AddTelemetry(res.RequestID, telemetryBytes) + default: + if !disp.collectData { + return + } + } + + // Harvest and Send agent Data to New Relic + if force { + harvestAgentTelemetry(ctx, disp.batch.Harvest(force), disp.telemetryClient, res.InvokedFunctionArn) + } else { + if disp.batch.ReadyToHarvest() { + harvestData := disp.batch.Harvest(false) + harvestAgentTelemetry(ctx, harvestData, disp.telemetryClient, res.InvokedFunctionArn) + } + } +} + +// harvests and sends agent telemetry to New Relic +func harvestAgentTelemetry(ctx context.Context, harvested []*Invocation, telemetryClient *Client, functionARN string) { + if len(harvested) > 0 { + l.Debugf("[agentTelemetry] sending agent harvest with %d invocations", len(harvested)) + telemetrySlice := make([][]byte, 0, 2*len(harvested)) + for _, inv := range harvested { + telemetrySlice = append(telemetrySlice, inv.Telemetry...) + } + + numSuccessful, err := telemetryClient.SendTelemetry(ctx, functionARN, telemetrySlice) + if err != nil { + l.Errorf("[agentTelemetry] failed to send harvested telemetry for %d invocations %v", len(harvested)-numSuccessful, err) + } + } +} diff --git a/telemetry/ipc.go b/agentTelemetry/ipc.go similarity index 65% rename from telemetry/ipc.go rename to agentTelemetry/ipc.go index 1ce805b8..c5bdb92a 100644 --- a/telemetry/ipc.go +++ b/agentTelemetry/ipc.go @@ -1,12 +1,9 @@ -package telemetry +package agentTelemetry import ( - "io/ioutil" - "log" + "io" "os" "syscall" - - "github.com/newrelic/newrelic-lambda-extension/util" ) const telemetryNamedPipePath = "/tmp/newrelic-telemetry" @@ -19,6 +16,7 @@ func InitTelemetryChannel() (chan []byte, error) { return nil, err } + // buffer channel to avoid deadlocks telemetryChan := make(chan []byte) go func() { @@ -34,15 +32,20 @@ func pollForTelemetry() []byte { // Opening a pipe will block, until the write side has been opened as well telemetryPipe, err := os.OpenFile(telemetryNamedPipePath, os.O_RDONLY, 0) if err != nil { - log.Panic("failed to open telemetry pipe", err) + l.Fatal("[pollForTelemetry] failed to open telemetry pipe", err) } - defer util.Close(telemetryPipe) + defer func(telemetryPipe *os.File) { + err := telemetryPipe.Close() + if err != nil { + l.Warn(err) + } + }(telemetryPipe) // When the write side closes, we get an EOF. - bytes, err := ioutil.ReadAll(telemetryPipe) + bytes, err := io.ReadAll(telemetryPipe) if err != nil { - log.Panic("failed to read telemetry pipe", err) + l.Fatal("[pollForTelemetry] failed to read telemetry pipe", err) } return bytes diff --git a/telemetry/ipc_test.go b/agentTelemetry/ipc_test.go similarity index 89% rename from telemetry/ipc_test.go rename to agentTelemetry/ipc_test.go index b8704f53..e6492c77 100644 --- a/telemetry/ipc_test.go +++ b/agentTelemetry/ipc_test.go @@ -1,4 +1,4 @@ -package telemetry +package agentTelemetry import ( "testing" diff --git a/telemetry/payload.go b/agentTelemetry/payload.go similarity index 95% rename from telemetry/payload.go rename to agentTelemetry/payload.go index 5b96bb96..0c74156a 100644 --- a/telemetry/payload.go +++ b/agentTelemetry/payload.go @@ -1,4 +1,4 @@ -package telemetry +package agentTelemetry import ( "bytes" @@ -74,7 +74,7 @@ func ExtractTraceID(data []byte) (string, error) { dataSegment, ok := segments["data"] if !ok { - return "", errors.New("No trace ID found in payload") + return "", errors.New("no trace ID found in payload") } analyticEvents, ok := dataSegment["analytic_event_data"] @@ -119,5 +119,5 @@ func ExtractTraceID(data []byte) (string, error) { } } - return "", errors.New("No trace ID found in payload") + return "", errors.New("no trace ID found in payload") } diff --git a/telemetry/payload_test.go b/agentTelemetry/payload_test.go similarity index 99% rename from telemetry/payload_test.go rename to agentTelemetry/payload_test.go index 486069ce..20c515cf 100644 --- a/telemetry/payload_test.go +++ b/agentTelemetry/payload_test.go @@ -1,4 +1,4 @@ -package telemetry +package agentTelemetry import ( "encoding/base64" @@ -73,7 +73,7 @@ func TestExtractTraceIDInvalid(t *testing.T) { assert.Empty(t, traceId) payload2 := []byte("[foobar]") - payload2 = []byte(base64.StdEncoding.EncodeToString(payload)) + payload2 = []byte(base64.StdEncoding.EncodeToString(payload2)) traceId, err = ExtractTraceID(payload2) diff --git a/telemetry/request.go b/agentTelemetry/request.go similarity index 86% rename from telemetry/request.go rename to agentTelemetry/request.go index d72f82fe..69000625 100644 --- a/telemetry/request.go +++ b/agentTelemetry/request.go @@ -1,4 +1,4 @@ -package telemetry +package agentTelemetry import ( "bytes" @@ -7,7 +7,7 @@ import ( "fmt" "net/http" - "github.com/newrelic/newrelic-lambda-extension/util" + "newrelic-lambda-extension/util" ) const ( @@ -89,7 +89,7 @@ func LogsEventForBytes(payload []byte) LogsEvent { return LogsEvent{ID: util.UUID(), Message: string(payload), Timestamp: util.Timestamp()} } -func CompressedPayloadsForLogEvents(logsEvents []LogsEvent, functionName string, invokedFunctionARN string) ([]*bytes.Buffer, error) { +func CompressedPayloadsForLogEvents(ct *util.CompressTool, logsEvents []LogsEvent, functionName string, invokedFunctionARN string) ([]*bytes.Buffer, error) { logGroupName := fmt.Sprintf("/aws/lambda/%s", functionName) logEntry := LogsEntry{ LogEvents: logsEvents, @@ -109,7 +109,7 @@ func CompressedPayloadsForLogEvents(logsEvents []LogsEvent, functionName string, } data := RequestData{Context: context, Entry: string(entry)} - compressed, err := CompressedJsonPayload(data) + compressed, err := CompressedJsonPayload(ct, data) if err != nil { return nil, err } @@ -120,12 +120,12 @@ func CompressedPayloadsForLogEvents(logsEvents []LogsEvent, functionName string, } else { // Payload is too large, split in half, recursively split := len(logsEvents) / 2 - leftRet, err := CompressedPayloadsForLogEvents(logsEvents[0:split], functionName, invokedFunctionARN) + leftRet, err := CompressedPayloadsForLogEvents(ct, logsEvents[0:split], functionName, invokedFunctionARN) if err != nil { return nil, err } - rightRet, err := CompressedPayloadsForLogEvents(logsEvents[split:], functionName, invokedFunctionARN) + rightRet, err := CompressedPayloadsForLogEvents(ct, logsEvents[split:], functionName, invokedFunctionARN) if err != nil { return nil, err } @@ -149,13 +149,13 @@ func BuildVortexRequest(ctx context.Context, url string, compressed *bytes.Buffe return req, nil } -func CompressedJsonPayload(payload interface{}) (*bytes.Buffer, error) { +func CompressedJsonPayload(ct *util.CompressTool, payload interface{}) (*bytes.Buffer, error) { uncompressed, err := json.Marshal(payload) if err != nil { return nil, err } - compressed, err := util.Compress(uncompressed) + compressed, err := ct.Compress(uncompressed) if err != nil { return nil, fmt.Errorf("error compressing data: %v", err) } diff --git a/telemetry/request_test.go b/agentTelemetry/request_test.go similarity index 97% rename from telemetry/request_test.go rename to agentTelemetry/request_test.go index beaf1cb4..b2efc65b 100644 --- a/telemetry/request_test.go +++ b/agentTelemetry/request_test.go @@ -1,9 +1,10 @@ -package telemetry +package agentTelemetry import ( "encoding/json" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestSerialize_DetailedFunctionLog(t *testing.T) { diff --git a/agentTelemetry/util_test.go b/agentTelemetry/util_test.go new file mode 100644 index 00000000..401690a9 --- /dev/null +++ b/agentTelemetry/util_test.go @@ -0,0 +1,17 @@ +package agentTelemetry + +import ( + "fmt" + "testing" +) + +type mockCloseable struct{} + +func (mockCloseable) Close() error { + return fmt.Errorf("Something went wrong") +} + +func TestClose(t *testing.T) { + c := &mockCloseable{} + Close(c) +} diff --git a/checks/agent_version_check.go b/checks/agent_version_check.go deleted file mode 100644 index 5dea9b1c..00000000 --- a/checks/agent_version_check.go +++ /dev/null @@ -1,56 +0,0 @@ -package checks - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "path/filepath" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/util" - "golang.org/x/mod/semver" -) - -type LayerAgentVersion struct { - Version string `json:"version"` -} - -// We are only returning an error message when an out of date agent version is detected. -// All other errors will result in a nil return value. -func agentVersionCheck(ctx context.Context, conf *config.Configuration, reg *api.RegistrationResponse, r runtimeConfig) error { - if r.AgentVersion == "" { - return nil - } - - v := LayerAgentVersion{} - - for i := range r.layerAgentPaths { - f := filepath.Join(r.layerAgentPaths[i], r.agentVersionFile) - if !util.PathExists(f) { - continue - } - - b, err := ioutil.ReadFile(f) - if err != nil { - return nil - } - - if r.language == Python { - v.Version = string(b) - } else { - err = json.Unmarshal([]byte(b), &v) - if err != nil { - return nil - } - } - } - - // semver requires a prepended v on version string - if v.Version != "" && semver.Compare("v"+v.Version, r.AgentVersion) < 0 { - return fmt.Errorf("Agent version out of date: v%s, in order access up to date features please upgrade to the latest New Relic %s layer that includes agent version %s", v.Version, r.language, r.AgentVersion) - } - - return nil -} diff --git a/checks/agent_version_check_test.go b/checks/agent_version_check_test.go deleted file mode 100644 index 7ba2379d..00000000 --- a/checks/agent_version_check_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package checks - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/stretchr/testify/assert" -) - -func TestAgentVersion(t *testing.T) { - conf := config.Configuration{} - reg := api.RegistrationResponse{} - r := runtimeConfig{} - ctx := context.Background() - - // No version set - err := agentVersionCheck(ctx, &conf, ®, r) - assert.Nil(t, err) - - // Error - dirname, err := os.MkdirTemp("", "") - assert.Nil(t, err) - defer os.RemoveAll(dirname) - - testFile := filepath.Join(dirname, "opt", "python", "lib", "python3.8", "site-packages", "newrelic") - r = runtimeConfigs[Python] - r.AgentVersion = "v10.1.2" - r.layerAgentPaths = []string{testFile} - - os.MkdirAll(testFile, os.ModePerm) - f, _ := os.Create(filepath.Join(testFile, r.agentVersionFile)) - f.WriteString("10.1.0") - - err = agentVersionCheck(ctx, &conf, ®, r) - assert.EqualError(t, err, "Agent version out of date: v10.1.0, in order access up to date features please upgrade to the latest New Relic python layer that includes agent version v10.1.2") - - // Success - r.AgentVersion = "10.1.0" - err = agentVersionCheck(ctx, &conf, ®, r) - assert.Nil(t, err) -} diff --git a/checks/handler_check.go b/checks/handler_check.go deleted file mode 100644 index 73d120a7..00000000 --- a/checks/handler_check.go +++ /dev/null @@ -1,59 +0,0 @@ -package checks - -import ( - "context" - "fmt" - "strings" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/util" -) - -type handlerConfigs struct { - handlerName string - conf *config.Configuration -} - -var handlerPath = "/var/task" - -func handlerCheck(ctx context.Context, conf *config.Configuration, reg *api.RegistrationResponse, r runtimeConfig) error { - if r.language != "" { - h := handlerConfigs{ - handlerName: reg.Handler, - conf: conf, - } - - if !r.check(h) { - return fmt.Errorf("Missing handler file %s (NEW_RELIC_LAMBDA_HANDLER=%s)", h.handlerName, conf.NRHandler) - } - } - - return nil -} - -func (r runtimeConfig) check(h handlerConfigs) bool { - functionHandler := r.getTrueHandler(h) - p := removePathMethodName(functionHandler) - p = pathFormatter(p, r.fileType) - return util.PathExists(p) -} - -func (r runtimeConfig) getTrueHandler(h handlerConfigs) string { - if h.handlerName != r.wrapperName { - util.Logln("Warning: handler not set to New Relic layer wrapper", r.wrapperName) - return h.handlerName - } - - return h.conf.NRHandler -} - -func removePathMethodName(p string) string { - s := strings.Split(p, ".") - return strings.Join(s[:len(s)-1], "/") -} - -func pathFormatter(functionHandler string, fileType string) string { - p := fmt.Sprintf("%s/%s.%s", handlerPath, functionHandler, fileType) - return p -} diff --git a/checks/handler_check_test.go b/checks/handler_check_test.go deleted file mode 100644 index deed06ad..00000000 --- a/checks/handler_check_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package checks - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/stretchr/testify/assert" -) - -var testHandler = "path/to/app.handler" - -func TestRuntimeMethods(t *testing.T) { - conf := config.Configuration{} - r := runtimeConfigs[Node] - h := handlerConfigs{ - handlerName: r.wrapperName, - conf: &conf, - } - conf.NRHandler = testHandler - - t1 := r.getTrueHandler(h) - t2 := removePathMethodName(t1) - t3 := pathFormatter(t2, r.fileType) - - e1 := testHandler - e2 := "path/to/app" - e3 := "/var/task/path/to/app.js" - - assert.Equal(t, e1, t1) - assert.Equal(t, e2, t2) - assert.Equal(t, e3, t3) - - r = runtimeConfigs[Python] - - h = handlerConfigs{ - handlerName: r.wrapperName, - conf: &conf, - } - - t1 = r.getTrueHandler(h) - t2 = removePathMethodName(t1) - t3 = pathFormatter(t2, r.fileType) - - e1 = testHandler - e2 = "path/to/app" - e3 = "/var/task/path/to/app.py" - - assert.Equal(t, e1, t1) - assert.Equal(t, e2, t2) - assert.Equal(t, e3, t3) -} - -func TestHandlerCheck(t *testing.T) { - conf := config.Configuration{} - reg := api.RegistrationResponse{} - r := runtimeConfigs[Node] - ctx := context.Background() - - // No Runtime - err := handlerCheck(ctx, &conf, ®, runtimeConfig{}) - assert.Nil(t, err) - - // Error - reg.Handler = testHandler - conf.NRHandler = config.EmptyNRWrapper - err = handlerCheck(ctx, &conf, ®, r) - assert.EqualError(t, err, "Missing handler file path/to/app.handler (NEW_RELIC_LAMBDA_HANDLER=Undefined)") - - // Success - dirname, err := os.MkdirTemp("", "") - assert.Nil(t, err) - defer os.RemoveAll(dirname) - - handlerPath = filepath.Join(dirname, "var", "task") - os.MkdirAll(filepath.Join(handlerPath, "path", "to"), os.ModePerm) - os.Create(filepath.Join(handlerPath, "path", "to", "app.js")) - - reg.Handler = testHandler - conf.NRHandler = config.EmptyNRWrapper - err = handlerCheck(ctx, &conf, ®, r) - assert.Nil(t, err) -} diff --git a/checks/runtime_check.go b/checks/runtime_check.go deleted file mode 100644 index 8bb3917f..00000000 --- a/checks/runtime_check.go +++ /dev/null @@ -1,62 +0,0 @@ -package checks - -import ( - "context" - "net/http" - "path/filepath" - "regexp" - "time" - - "github.com/google/go-github/v44/github" - "github.com/newrelic/newrelic-lambda-extension/util" -) - -type httpClient interface { - Get(string) (*http.Response, error) -} - -var ( - client httpClient - githubClient *github.Client - re = regexp.MustCompile(`\/releases\/tag\/(v[0-9.]+)`) -) - -func init() { - client = &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - Timeout: time.Second * 10, - } - githubClient = github.NewClient(&http.Client{Timeout: time.Second * 10}) -} - -func checkAndReturnRuntime() (runtimeConfig, error) { - for k, v := range runtimeConfigs { - p := filepath.Join(runtimeLookupPath, string(k)) - if util.PathExists(p) { - err := latestAgentTag(&v) - return v, err - } - } - - // If we make it here that means the runtime is not one we - // currently validate so we don't want to warn against anything - return runtimeConfig{}, nil -} - -func latestAgentTag(r *runtimeConfig) error { - ctx := context.Background() - release, _, err := githubClient.Repositories.GetLatestRelease(ctx, r.agentVersionGitOrg, r.agentVersionGitRepo) - - if err != nil { - util.Debugf("Could not retrieve latest GitHub release: %v", err) - return nil - } - - if release.TagName != nil { - r.AgentVersion = *release.TagName - } - - return nil -} diff --git a/checks/runtime_check_test.go b/checks/runtime_check_test.go deleted file mode 100644 index f20f267d..00000000 --- a/checks/runtime_check_test.go +++ /dev/null @@ -1,49 +0,0 @@ -//go:build !race -// +build !race - -package checks - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRuntimeCheck(t *testing.T) { - dirname, err := os.MkdirTemp("", "") - assert.Nil(t, err) - defer os.RemoveAll(dirname) - - oldPath := runtimeLookupPath - defer func() { - runtimeLookupPath = oldPath - }() - runtimeLookupPath = filepath.Join(dirname, runtimeLookupPath) - - os.MkdirAll(filepath.Join(runtimeLookupPath, "node"), os.ModePerm) - r, err := checkAndReturnRuntime() - assert.Equal(t, runtimeConfigs[Node].language, r.language) - assert.Nil(t, err) -} - -func TestRuntimeCheckNil(t *testing.T) { - r, err := checkAndReturnRuntime() - assert.Equal(t, runtimeConfig{}, r) - assert.Nil(t, err) -} - -func TestLatestAgentTag(t *testing.T) { - r := &runtimeConfig{agentVersionGitOrg: runtimeConfigs[Python].agentVersionGitOrg, agentVersionGitRepo: runtimeConfigs[Python].agentVersionGitRepo} - err := latestAgentTag(r) - assert.NotEmpty(t, r.AgentVersion) - assert.Nil(t, err) -} - -func TestLatestAgentTagError(t *testing.T) { - r := &runtimeConfig{agentVersionGitOrg: "", agentVersionGitRepo: ""} - err := latestAgentTag(r) - assert.Empty(t, r.AgentVersion) - assert.Nil(t, err) -} diff --git a/checks/runtime_config.go b/checks/runtime_config.go deleted file mode 100644 index 639740ec..00000000 --- a/checks/runtime_config.go +++ /dev/null @@ -1,58 +0,0 @@ -package checks - -var ( - layerAgentPathNode = []string{"/opt/nodejs/node_modules/newrelic"} - layerAgentPathsPython = []string{ - "/opt/python/lib/python2.7/site-packages/newrelic", - "/opt/python/lib/python3.6/site-packages/newrelic", - "/opt/python/lib/python3.7/newrelic", - "/opt/python/lib/python3.8/site-packages/newrelic", - "/opt/python/lib/python3.9/site-packages/newrelic", - } - vendorAgentPathNode = "/var/task/node_modules/newrelic" - vendorAgentPathPython = "/var/task/newrelic" - runtimeLookupPath = "/var/lang/bin" -) - -type runtimeConfig struct { - AgentVersion string - agentVersionGitOrg string - agentVersionGitRepo string - agentVersionFile string - fileType string - language Runtime - layerAgentPaths []string - vendorAgentPath string - wrapperName string -} - -type Runtime string - -const ( - Python Runtime = "python" - Node Runtime = "node" -) - -// Runtime static values -var runtimeConfigs = map[Runtime]runtimeConfig{ - Node: { - language: Node, - wrapperName: "newrelic-lambda-wrapper.handler", - fileType: "js", - layerAgentPaths: layerAgentPathNode, - vendorAgentPath: vendorAgentPathNode, - agentVersionFile: "package.json", - agentVersionGitOrg: "newrelic", - agentVersionGitRepo: "node-newrelic", - }, - Python: { - language: Python, - wrapperName: "newrelic_lambda_wrapper.handler", - fileType: "py", - layerAgentPaths: layerAgentPathsPython, - vendorAgentPath: vendorAgentPathPython, - agentVersionFile: "version.txt", - agentVersionGitOrg: "newrelic", - agentVersionGitRepo: "newrelic-python-agent", - }, -} diff --git a/checks/sanity_check.go b/checks/sanity_check.go deleted file mode 100644 index 64dd0666..00000000 --- a/checks/sanity_check.go +++ /dev/null @@ -1,35 +0,0 @@ -package checks - -import ( - "context" - "fmt" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/credentials" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/util" -) - -var ( - awsLogIngestionEnvVars = []string{ - "DEBUG_LOGGING_ENABLED", - "INFRA_ENABLED", - "LICENSE_KEY", - "LOGGING_ENABLED", - "NR_INFRA_ENDPOINT", - "NR_LOGGING_ENDPOINT", - } -) - -// sanityCheck checks for configuration that is either misplaced or in conflict -func sanityCheck(ctx context.Context, conf *config.Configuration, res *api.RegistrationResponse, _ runtimeConfig) error { - if util.AnyEnvVarsExist(awsLogIngestionEnvVars) { - return fmt.Errorf("Environment varaible '%s' is used by aws-log-ingestion and has no effect here. Recommend unsetting this environment variable within this function.", util.AnyEnvVarsExistString(awsLogIngestionEnvVars)) - } - - if credentials.IsSecretConfigured(ctx, conf) && util.EnvVarExists("NEW_RELIC_LICENSE_KEY") { - return fmt.Errorf("There is both a AWS Secrets Manager secret and a NEW_RELIC_LICENSE_KEY environment variable set. Recommend removing the NEW_RELIC_LICENSE_KEY environment variable and using the AWS Secrets Manager secret.") - } - - return nil -} diff --git a/checks/sanity_check_test.go b/checks/sanity_check_test.go deleted file mode 100644 index 0919d9c6..00000000 --- a/checks/sanity_check_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package checks - -import ( - "context" - "fmt" - "os" - "testing" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/secretsmanager" - "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/credentials" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/util" - "github.com/stretchr/testify/assert" -) - -type mockSecretManager struct { - secretsmanageriface.SecretsManagerAPI -} - -func (mockSecretManager) GetSecretValueWithContext(context.Context, *secretsmanager.GetSecretValueInput, ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { - return &secretsmanager.GetSecretValueOutput{ - SecretString: aws.String(`{"LicenseKey": "foo"}`), - }, nil -} - -type mockSecretManagerErr struct { - secretsmanageriface.SecretsManagerAPI -} - -func (mockSecretManagerErr) GetSecretValueWithContext(context.Context, *secretsmanager.GetSecretValueInput, ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { - return nil, fmt.Errorf("Something went wrong") -} - -func TestSanityCheck(t *testing.T) { - ctx := context.Background() - - if util.AnyEnvVarsExist(awsLogIngestionEnvVars) { - assert.Error(t, sanityCheck(ctx, &config.Configuration{}, &api.RegistrationResponse{}, runtimeConfig{})) - } else { - assert.Nil(t, sanityCheck(ctx, &config.Configuration{}, &api.RegistrationResponse{}, runtimeConfig{})) - } - - os.Setenv("DEBUG_LOGGING_ENABLED", "1") - assert.Error(t, sanityCheck(ctx, &config.Configuration{}, &api.RegistrationResponse{}, runtimeConfig{})) - os.Unsetenv("DEBUG_LOGGING_ENABLED") - - os.Unsetenv("NEW_RELIC_LICENSE_KEY") - credentials.OverrideSecretsManager(&mockSecretManager{}) - assert.Nil(t, sanityCheck(ctx, &config.Configuration{}, &api.RegistrationResponse{}, runtimeConfig{})) - - os.Setenv("NEW_RELIC_LICENSE_KEY", "foobar") - defer os.Unsetenv("NEW_RELIC_LICENSE_KEY") - credentials.OverrideSecretsManager(&mockSecretManager{}) - assert.Error(t, sanityCheck(ctx, &config.Configuration{}, &api.RegistrationResponse{}, runtimeConfig{})) - - credentials.OverrideSecretsManager(&mockSecretManagerErr{}) - assert.Nil(t, sanityCheck(ctx, &config.Configuration{}, &api.RegistrationResponse{}, runtimeConfig{})) -} diff --git a/checks/startup_check.go b/checks/startup_check.go deleted file mode 100644 index df600a7f..00000000 --- a/checks/startup_check.go +++ /dev/null @@ -1,57 +0,0 @@ -package checks - -import ( - "context" - "fmt" - "time" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/lambda/logserver" - "github.com/newrelic/newrelic-lambda-extension/util" -) - -type checkFn func(context.Context, *config.Configuration, *api.RegistrationResponse, runtimeConfig) error - -type LogSender interface { - SendFunctionLogs(ctx context.Context, invokedFunctionARN string, lines []logserver.LogLine) error -} - -/// Register checks here -var checks = []checkFn{ - agentVersionCheck, - handlerCheck, - sanityCheck, - vendorCheck, -} - -func RunChecks(ctx context.Context, conf *config.Configuration, reg *api.RegistrationResponse, logSender LogSender) { - runtimeConfig, err := checkAndReturnRuntime() - if err != nil { - errLog := fmt.Sprintf("There was an issue querying for the latest agent version: %v", err) - util.Logln(errLog) - } - - for _, check := range checks { - runCheck(ctx, conf, reg, runtimeConfig, logSender, check) - } -} - -func runCheck(ctx context.Context, conf *config.Configuration, reg *api.RegistrationResponse, r runtimeConfig, logSender LogSender, check checkFn) error { - err := check(ctx, conf, reg, r) - if err != nil { - errLog := fmt.Sprintf("Startup check failed: %v", err) - util.Logln(errLog) - - //Send a log line to NR as well - logSender.SendFunctionLogs(ctx, "", []logserver.LogLine{ - { - Time: time.Now(), - RequestID: "0", - Content: []byte(errLog), - }, - }) - } - - return err -} diff --git a/checks/startup_check_test.go b/checks/startup_check_test.go deleted file mode 100644 index f66359c3..00000000 --- a/checks/startup_check_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package checks - -import ( - "context" - "errors" - "fmt" - "net/http" - "testing" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/lambda/logserver" - "github.com/stretchr/testify/assert" -) - -type mockClientError struct{} - -func (c *mockClientError) Get(string) (*http.Response, error) { - return nil, errors.New("Something went wrong") -} - -type TestLogSender struct { - sent []logserver.LogLine -} - -func (c *TestLogSender) SendFunctionLogs(ctx context.Context, invokedFunctionARN string, lines []logserver.LogLine) error { - c.sent = append(c.sent, lines...) - return nil -} - -func TestRunCheck(t *testing.T) { - conf := config.Configuration{} - resp := api.RegistrationResponse{} - r := runtimeConfig{} - client := TestLogSender{} - ctx := context.Background() - - tested := false - testCheck := func(ctx context.Context, conf *config.Configuration, resp *api.RegistrationResponse, r runtimeConfig) error { - tested = true - return nil - } - - result := runCheck(ctx, &conf, &resp, r, &client, testCheck) - - assert.Equal(t, true, tested) - assert.Nil(t, result) -} - -func TestRunCheckErr(t *testing.T) { - conf := config.Configuration{} - resp := api.RegistrationResponse{} - r := runtimeConfig{} - logSender := TestLogSender{} - ctx := context.Background() - - tested := false - testCheck := func(ctx context.Context, conf *config.Configuration, resp *api.RegistrationResponse, r runtimeConfig) error { - tested = true - return fmt.Errorf("Failure Test") - } - - result := runCheck(ctx, &conf, &resp, r, &logSender, testCheck) - - assert.Equal(t, true, tested) - assert.NotNil(t, result) - - assert.Equal(t, "Startup check failed: Failure Test", string(logSender.sent[0].Content)) -} - -func TestRunChecks(t *testing.T) { - c := &config.Configuration{} - r := &api.RegistrationResponse{} - l := &TestLogSender{} - - client = &mockClientError{} - - ctx := context.Background() - RunChecks(ctx, c, r, l) -} diff --git a/checks/vendor_check.go b/checks/vendor_check.go deleted file mode 100644 index 59b12ac9..00000000 --- a/checks/vendor_check.go +++ /dev/null @@ -1,21 +0,0 @@ -package checks - -import ( - "context" - "fmt" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/util" -) - -// vendorCheck checks to see if the user included a vendored copy of the agent along -// with their function while also using a layer that includes the agent -func vendorCheck(ctx context.Context, _ *config.Configuration, _ *api.RegistrationResponse, r runtimeConfig) error { - - if util.PathExists(r.vendorAgentPath) && util.AnyPathsExist(r.layerAgentPaths) { - return fmt.Errorf("Vendored agent found at '%s', a layer already includes this agent at '%s'. Recommend using the layer agent to avoid unexpected agent behavior.", r.vendorAgentPath, util.AnyPathsExistString(r.layerAgentPaths)) - } - - return nil -} diff --git a/checks/vendor_check_test.go b/checks/vendor_check_test.go deleted file mode 100644 index 9e037cd8..00000000 --- a/checks/vendor_check_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package checks - -import ( - "context" - "testing" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/util" - "github.com/stretchr/testify/assert" -) - -func TestVendorCheck(t *testing.T) { - n := runtimeConfigs[Node] - ctx := context.Background() - - if !util.AnyPathsExist(n.layerAgentPaths) && !util.PathExists(n.vendorAgentPath) { - assert.Nil(t, vendorCheck(ctx, &config.Configuration{}, &api.RegistrationResponse{}, n)) - } - - if util.PathExists(n.layerAgentPaths[0]) && util.PathExists(n.vendorAgentPath) { - assert.Error(t, vendorCheck(ctx, &config.Configuration{}, &api.RegistrationResponse{}, n)) - } - - p := runtimeConfigs[Python] - - if !util.AnyPathsExist(p.layerAgentPaths) && !util.PathExists(p.vendorAgentPath) { - assert.Nil(t, vendorCheck(ctx, &config.Configuration{}, &api.RegistrationResponse{}, n)) - } - - if util.AnyPathsExist(p.layerAgentPaths) && util.PathExists(p.vendorAgentPath) { - assert.Error(t, vendorCheck(ctx, &config.Configuration{}, &api.RegistrationResponse{}, n)) - } -} diff --git a/config/config.go b/config/config.go index 1e0b8e50..48e5f76b 100644 --- a/config/config.go +++ b/config/config.go @@ -2,136 +2,125 @@ package config import ( "os" + "path" "strconv" "strings" "time" -) -const ( - DefaultRipeMillis = 7_000 - DefaultRotMillis = 12_000 - DefaultLogLevel = "INFO" - DebugLogLevel = "DEBUG" - defaultLogServerHost = "sandbox.localdomain" - DefaultClientTimeout = 10 * time.Second + log "github.com/sirupsen/logrus" ) -var EmptyNRWrapper = "Undefined" - -type Configuration struct { - ExtensionEnabled bool - LogsEnabled bool - SendFunctionLogs bool - CollectTraceID bool - RipeMillis uint32 - RotMillis uint32 - LicenseKey string - LicenseKeySecretId string - NRHandler string - TelemetryEndpoint string - LogEndpoint string - LogLevel string - LogServerHost string - ClientTimeout time.Duration +type Config struct { + DataCollectionTimeout time.Duration + LogLevel log.Level + AgentTelemetryBatchSize int + TelemetryAPIBatchSize int64 + AgentTelemetryRegion string + LicenseKey string + AccountID string + ExtensionName string + CollectAgentData bool } -func ConfigurationFromEnvironment() *Configuration { - enabledStr, extensionEnabledOverride := os.LookupEnv("NEW_RELIC_LAMBDA_EXTENSION_ENABLED") - licenseKey, lkOverride := os.LookupEnv("NEW_RELIC_LICENSE_KEY") - licenseKeySecretId, lkSecretOverride := os.LookupEnv("NEW_RELIC_LICENSE_KEY_SECRET") - nrHandler, nrOverride := os.LookupEnv("NEW_RELIC_LAMBDA_HANDLER") - telemetryEndpoint, teOverride := os.LookupEnv("NEW_RELIC_TELEMETRY_ENDPOINT") - logEndpoint, leOverride := os.LookupEnv("NEW_RELIC_LOG_ENDPOINT") - clientTimeout, ctOverride := os.LookupEnv("NEW_RELIC_DATA_COLLECTION_TIMEOUT") - ripeMillisStr, ripeMillisOverride := os.LookupEnv("NEW_RELIC_HARVEST_RIPE_MILLIS") - rotMillisStr, rotMillisOverride := os.LookupEnv("NEW_RELIC_HARVEST_ROT_MILLIS") - logLevelStr, logLevelOverride := os.LookupEnv("NEW_RELIC_EXTENSION_LOG_LEVEL") - logsEnabledStr, logsEnabledOverride := os.LookupEnv("NEW_RELIC_EXTENSION_LOGS_ENABLED") - sendFunctionLogsStr, sendFunctionLogsOverride := os.LookupEnv("NEW_RELIC_EXTENSION_SEND_FUNCTION_LOGS") - logServerHostStr, logServerHostOverride := os.LookupEnv("NEW_RELIC_LOG_SERVER_HOST") - collectTraceIDStr, collectTraceIDOverride := os.LookupEnv("NEW_RELIC_COLLECT_TRACE_ID") - - extensionEnabled := true - if extensionEnabledOverride && strings.ToLower(enabledStr) == "false" { - extensionEnabled = false - } - - logsEnabled := true - if logsEnabledOverride && strings.ToLower(logsEnabledStr) == "false" { - logsEnabled = false - } - - ret := &Configuration{ExtensionEnabled: extensionEnabled, LogsEnabled: logsEnabled} - - ret.ClientTimeout = DefaultClientTimeout - if ctOverride && clientTimeout != "" { - clientTimeout, err := time.ParseDuration(clientTimeout) - if err == nil { - ret.ClientTimeout = clientTimeout - } - } +const ( + defaultCollectionTimeout = 10 * time.Second + minimumCollectionTimeout = 600 * time.Millisecond + defaultAgentTelemtryBatchSize = 1 + defaultTelemtryAPIBatchSize = 1 + + // Optional Environment variables that can be used to talior the user experience to your needs + agentDataEnabledVariable = "NEW_RELIC_EXTENSION_AGENT_DATA_COLLECTION_ENABLED" + agentDataBatchSizeVariable = "NEW_RELIC_EXTENSION_AGENT_DATA_BATCH_SIZE" + clientRetryTimeoutVariable = "NEW_RELIC_EXTENSION_DATA_COLLECTION_TIMEOUT" + agentTelemetryRegionVariable = "NEW_RELIC_EXTENSION_COLLECTOR_OVERRIDE" + extensionLogLevelVariable = "NEW_RELIC_EXTENSION_LOG_LEVEL" + telAPIBatchSizeVariable = "NEW_RELIC_EXTENSION_TELEMETRY_API_BATCH_SIZE" + + // Required environment variable + nrAccountIDVariable = "NEW_RELIC_ACCOUNT_ID" +) - if lkOverride { - ret.LicenseKey = licenseKey - } else if lkSecretOverride { - ret.LicenseKeySecretId = licenseKeySecretId +var l = log.WithFields(log.Fields{"pkg": "config"}) + +// simplifies testing +func defaultConfig() Config { + return Config{ + CollectAgentData: true, + DataCollectionTimeout: defaultCollectionTimeout, + AgentTelemetryBatchSize: defaultAgentTelemtryBatchSize, + TelemetryAPIBatchSize: defaultTelemtryAPIBatchSize, + LogLevel: log.InfoLevel, + ExtensionName: path.Base(os.Args[0]), } +} - if nrOverride { - ret.NRHandler = nrHandler - } else { - ret.NRHandler = EmptyNRWrapper +func GetConfig() Config { + conf := defaultConfig() + conf.AccountID = os.Getenv(nrAccountIDVariable) + if conf.AccountID == "" { + l.Errorf("environment variable \"%s\" must be set to the ID of the New Relic account matching your license key", nrAccountIDVariable) } - if teOverride { - ret.TelemetryEndpoint = telemetryEndpoint - } + conf.AgentTelemetryRegion = os.Getenv(agentTelemetryRegionVariable) - if leOverride { - ret.LogEndpoint = logEndpoint + // Enable or disable collection of agent telemetry data + enableAgent := os.Getenv(agentDataEnabledVariable) + if strings.ToLower(enableAgent) == "false" { + conf.CollectAgentData = false } - - if ripeMillisOverride { - ripeMillis, err := strconv.ParseUint(ripeMillisStr, 10, 32) - if err == nil { - ret.RipeMillis = uint32(ripeMillis) + // How long agent will try to resend + clientTimeout := os.Getenv(clientRetryTimeoutVariable) + if clientTimeout != "" { + dur, err := time.ParseDuration(clientTimeout) + if err != nil { + environmentVariableError(clientRetryTimeoutVariable, err) + l.Warnf("client retry timeout will be set to default value: %s", defaultCollectionTimeout.String()) } - } - - if ret.RipeMillis == 0 { - ret.RipeMillis = DefaultRipeMillis - } - - if rotMillisOverride { - rotMillis, err := strconv.ParseUint(rotMillisStr, 10, 32) - if err == nil { - ret.RotMillis = uint32(rotMillis) + if dur >= minimumCollectionTimeout { + conf.DataCollectionTimeout = dur + } else { + l.Warnf("configured client retry duration is too short, setting it to minimum value: %s", minimumCollectionTimeout.String()) + conf.DataCollectionTimeout = minimumCollectionTimeout } } - if ret.RotMillis == 0 { - ret.RotMillis = DefaultRotMillis - } - - if logLevelOverride && logLevelStr == DebugLogLevel { - ret.LogLevel = DebugLogLevel - } else { - ret.LogLevel = DefaultLogLevel + telApiBatchSizeStr := os.Getenv(telAPIBatchSizeVariable) + if telApiBatchSizeStr != "" { + telApiBatchSize, err := strconv.ParseInt(telApiBatchSizeStr, 0, 16) + if err != nil { + environmentVariableError(telAPIBatchSizeVariable, err) + l.Warnf("telemetry api batch size will be set to default value: %d", defaultTelemtryAPIBatchSize) + } else { + conf.TelemetryAPIBatchSize = telApiBatchSize + } } - if logServerHostOverride { - ret.LogServerHost = logServerHostStr - } else { - ret.LogServerHost = defaultLogServerHost + buffer := os.Getenv(agentDataBatchSizeVariable) + if buffer != "" { + val, err := strconv.Atoi(buffer) + if err != nil { + environmentVariableError(agentDataBatchSizeVariable, err) + l.Warnf("agent data batch size will be set to default value: %d", defaultAgentTelemtryBatchSize) + } else { + conf.AgentTelemetryBatchSize = val + } } - if sendFunctionLogsOverride && sendFunctionLogsStr == "true" { - ret.SendFunctionLogs = true + logLevel := strings.ToLower(os.Getenv(extensionLogLevelVariable)) + switch logLevel { + case "trace": + conf.LogLevel = log.TraceLevel + case "debug": + conf.LogLevel = log.DebugLevel + case "info": + conf.LogLevel = log.InfoLevel + case "warn": + conf.LogLevel = log.WarnLevel } - if collectTraceIDOverride && collectTraceIDStr == "true" { - ret.CollectTraceID = true - } + return conf +} - return ret +func environmentVariableError(variable string, err error) { + l.Warnf("[config] error parsing environment variable \"%s\": %v", variable, err) } diff --git a/config/config_test.go b/config/config_test.go index 64fff68e..1fa31d85 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,89 +2,174 @@ package config import ( "os" + "path" + "reflect" "testing" + "time" - "github.com/stretchr/testify/assert" + log "github.com/sirupsen/logrus" ) -func TestConfigurationFromEnvironmentZero(t *testing.T) { - conf := ConfigurationFromEnvironment() - expected := &Configuration{ - ExtensionEnabled: true, - RipeMillis: DefaultRipeMillis, - RotMillis: DefaultRotMillis, - LogLevel: DefaultLogLevel, - LogsEnabled: true, - NRHandler: EmptyNRWrapper, - LogServerHost: defaultLogServerHost, - ClientTimeout: DefaultClientTimeout, - } - assert.Equal(t, expected, conf) +type envVariables struct { + agentData string + agentBatch string + telemBatch string + timeout string + region string + logLevel string + acctId string } -func TestConfigurationFromEnvironment(t *testing.T) { - os.Unsetenv("NEW_RELIC_LAMBDA_EXTENSION_ENABLED") - - conf := ConfigurationFromEnvironment() - - assert.Equal(t, conf.ExtensionEnabled, true) - assert.Equal(t, conf.LogsEnabled, true) - - os.Setenv("NEW_RELIC_LAMBDA_EXTENSION_ENABLED", "false") - os.Setenv("NEW_RELIC_LAMBDA_HANDLER", "newrelic_lambda_wrapper.handler") - os.Setenv("NEW_RELIC_LICENSE_KEY", "lk") - os.Setenv("NEW_RELIC_LICENSE_KEY_SECRET", "secretId") - os.Setenv("NEW_RELIC_LOG_ENDPOINT", "endpoint") - os.Setenv("NEW_RELIC_TELEMETRY_ENDPOINT", "endpoint") - os.Setenv("NEW_RELIC_HARVEST_RIPE_MILLIS", "0") - os.Setenv("NEW_RELIC_HARVEST_ROT_MILLIS", "0") - os.Setenv("NEW_RELIC_EXTENSION_LOG_LEVEL", "DEBUG") - os.Setenv("NEW_RELIC_EXTENSION_SEND_FUNCTION_LOGS", "true") - os.Setenv("NEW_RELIC_EXTENSION_LOGS_ENABLED", "false") - os.Setenv("NEW_RELIC_DATA_COLLECTION_TIMEOUT", "5s") - - defer func() { - os.Unsetenv("NEW_RELIC_LAMBDA_EXTENSION_ENABLED") - os.Unsetenv("NEW_RELIC_LAMBDA_HANDLER") - os.Unsetenv("NEW_RELIC_LICENSE_KEY") - os.Unsetenv("NEW_RELIC_LICENSE_KEY_SECRET") - os.Unsetenv("NEW_RELIC_LOG_ENDPOINT") - os.Unsetenv("NEW_RELIC_TELEMETRY_ENDPOINT") - os.Unsetenv("NEW_RELIC_HARVEST_RIPE_MILLIS") - os.Unsetenv("NEW_RELIC_HARVEST_ROT_MILLIS") - os.Unsetenv("NEW_RELIC_EXTENSION_LOG_LEVEL") - os.Unsetenv("NEW_RELIC_EXTENSION_SEND_FUNCTION_LOGS") - os.Unsetenv("NEW_RELIC_EXTENSION_LOGS_ENABLED") - os.Unsetenv("NEW_RELIC_DATA_COLLECTION_TIMEOUT") - }() - - conf = ConfigurationFromEnvironment() - - assert.Equal(t, conf.ExtensionEnabled, false) - assert.Equal(t, "newrelic_lambda_wrapper.handler", conf.NRHandler) - assert.Equal(t, "lk", conf.LicenseKey) - assert.Empty(t, conf.LicenseKeySecretId) - assert.Equal(t, "endpoint", conf.LogEndpoint) - assert.Equal(t, "endpoint", conf.TelemetryEndpoint) - assert.Equal(t, uint32(DefaultRipeMillis), conf.RipeMillis) - assert.Equal(t, uint32(DefaultRotMillis), conf.RotMillis) - assert.Equal(t, "DEBUG", conf.LogLevel) - assert.Equal(t, true, conf.SendFunctionLogs) - assert.Equal(t, false, conf.LogsEnabled) -} - -func TestConfigurationFromEnvironmentSecretId(t *testing.T) { - os.Setenv("NEW_RELIC_LICENSE_KEY_SECRET", "secretId") - defer os.Unsetenv("NEW_RELIC_LICENSE_KEY_SECRET") - - conf := ConfigurationFromEnvironment() - assert.Equal(t, "secretId", conf.LicenseKeySecretId) -} +func TestGetConfig(t *testing.T) { + tests := []struct { + name string + want Config + vars envVariables + }{ + { + name: "default", + want: defaultConfig(), + vars: envVariables{ + agentData: "", + agentBatch: "", + telemBatch: "", + timeout: "", + region: "", + logLevel: "", + acctId: "", + }, + }, + { + name: "default env vars", + want: defaultConfig(), + vars: envVariables{ + agentData: "true", + agentBatch: "1", + telemBatch: "1", + timeout: "10s", + region: "", + logLevel: "info", + acctId: "", + }, + }, + { + name: "override all defaults", + want: func() Config { + return Config{ + CollectAgentData: false, + DataCollectionTimeout: 600 * time.Millisecond, + AgentTelemetryBatchSize: 5, + TelemetryAPIBatchSize: 8, + LogLevel: log.WarnLevel, + AgentTelemetryRegion: "test", + AccountID: "12", + ExtensionName: path.Base(os.Args[0]), + } + }(), + vars: envVariables{ + agentData: "false", + agentBatch: "5", + telemBatch: "8", + timeout: "600ms", + region: "test", + logLevel: "warn", + acctId: "12", + }, + }, + { + name: "timeout too low", + want: func() Config { + return Config{ + CollectAgentData: false, + DataCollectionTimeout: 600 * time.Millisecond, + AgentTelemetryBatchSize: 5, + TelemetryAPIBatchSize: 8, + LogLevel: log.WarnLevel, + AgentTelemetryRegion: "test", + AccountID: "12", + ExtensionName: path.Base(os.Args[0]), + } + }(), + vars: envVariables{ + agentData: "false", + agentBatch: "5", + telemBatch: "8", + timeout: "300ms", + region: "test", + logLevel: "warn", + acctId: "12", + }, + }, + { + name: "invalid agent telemetry batch size", + want: func() Config { + return Config{ + CollectAgentData: false, + DataCollectionTimeout: 600 * time.Millisecond, + AgentTelemetryBatchSize: defaultAgentTelemtryBatchSize, + TelemetryAPIBatchSize: 8, + LogLevel: log.WarnLevel, + AgentTelemetryRegion: "test", + AccountID: "12", + ExtensionName: path.Base(os.Args[0]), + } + }(), + vars: envVariables{ + agentData: "false", + agentBatch: "invalid", + telemBatch: "8", + timeout: "300ms", + region: "test", + logLevel: "warn", + acctId: "12", + }, + }, + { + name: "invalid telemetry api batch size", + want: func() Config { + return Config{ + CollectAgentData: false, + DataCollectionTimeout: 600 * time.Millisecond, + AgentTelemetryBatchSize: 5, + TelemetryAPIBatchSize: defaultTelemtryAPIBatchSize, + LogLevel: log.WarnLevel, + AgentTelemetryRegion: "test", + AccountID: "12", + ExtensionName: path.Base(os.Args[0]), + } + }(), + vars: envVariables{ + agentData: "false", + agentBatch: "5", + telemBatch: "invalid", + timeout: "300ms", + region: "test", + logLevel: "warn", + acctId: "12", + }, + }, + } + for _, tt := range tests { + os.Setenv(agentDataEnabledVariable, tt.vars.agentData) + os.Setenv(agentDataBatchSizeVariable, tt.vars.agentBatch) + os.Setenv(clientRetryTimeoutVariable, tt.vars.timeout) + os.Setenv(agentTelemetryRegionVariable, tt.vars.region) + os.Setenv(extensionLogLevelVariable, tt.vars.logLevel) + os.Setenv(telAPIBatchSizeVariable, tt.vars.telemBatch) + os.Setenv(nrAccountIDVariable, tt.vars.acctId) -func TestConfigurationFromEnvironmentLogServerHost(t *testing.T) { - os.Setenv("NEW_RELIC_LOG_SERVER_HOST", "foobar") - defer os.Unsetenv("NEW_RELIC_LOG_SERVER_HOST") + t.Run(tt.name, func(t *testing.T) { + if got := GetConfig(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetConfig() = %v, want %v", got, tt.want) + } + }) - conf := ConfigurationFromEnvironment() - assert.Equal(t, "foobar", conf.LogServerHost) + os.Unsetenv(agentDataEnabledVariable) + os.Unsetenv(agentDataBatchSizeVariable) + os.Unsetenv(clientRetryTimeoutVariable) + os.Unsetenv(agentTelemetryRegionVariable) + os.Unsetenv(extensionLogLevelVariable) + os.Unsetenv(telAPIBatchSizeVariable) + os.Unsetenv(nrAccountIDVariable) + } } diff --git a/credentials/credentials.go b/credentials/credentials.go deleted file mode 100644 index bc520ed0..00000000 --- a/credentials/credentials.go +++ /dev/null @@ -1,99 +0,0 @@ -package credentials - -import ( - "context" - "encoding/json" - "fmt" - "os" - - "github.com/newrelic/newrelic-lambda-extension/util" - - "github.com/newrelic/newrelic-lambda-extension/config" - - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/secretsmanager" - "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" -) - -type licenseKeySecret struct { - LicenseKey string -} - -var ( - sess = session.Must(session.NewSessionWithOptions(session.Options{ - SharedConfigState: session.SharedConfigEnable, - })) - secrets secretsmanageriface.SecretsManagerAPI -) - -const defaultSecretId = "NEW_RELIC_LICENSE_KEY" - -func init() { - secrets = secretsmanager.New(sess) -} - -func getLicenseKeySecretId(conf *config.Configuration) string { - if conf.LicenseKeySecretId != "" { - util.Logln("Fetching license key from secret id " + conf.LicenseKeySecretId) - return conf.LicenseKeySecretId - } - - return defaultSecretId -} - -func decodeLicenseKey(rawJson *string) (string, error) { - var secrets licenseKeySecret - - err := json.Unmarshal([]byte(*rawJson), &secrets) - if err != nil { - return "", err - } - if secrets.LicenseKey == "" { - return "", fmt.Errorf("malformed license key secret; missing \"LicenseKey\" attribute") - } - - return secrets.LicenseKey, nil -} - -// IsSecretConfigured returns true if the Secrets Maanger secret is configured, false -// otherwise -func IsSecretConfigured(ctx context.Context, conf *config.Configuration) bool { - secretId := getLicenseKeySecretId(conf) - secretValueInput := secretsmanager.GetSecretValueInput{SecretId: &secretId} - - _, err := secrets.GetSecretValueWithContext(ctx, &secretValueInput) - if err != nil { - return false - } - - return true -} - -// GetNewRelicLicenseKey fetches the license key from AWS Secrets Manager, falling back -// to the NEW_RELIC_LICENSE_KEY environment variable if set. -func GetNewRelicLicenseKey(ctx context.Context, conf *config.Configuration) (string, error) { - if conf.LicenseKey != "" { - util.Logln("Using license key from environment variable") - return conf.LicenseKey, nil - } - - secretId := getLicenseKeySecretId(conf) - secretValueInput := secretsmanager.GetSecretValueInput{SecretId: &secretId} - - secretValueOutput, err := secrets.GetSecretValueWithContext(ctx, &secretValueInput) - if err != nil { - envLicenseKey, found := os.LookupEnv(defaultSecretId) - if found { - return envLicenseKey, nil - } - - return "", err - } - - return decodeLicenseKey(secretValueOutput.SecretString) -} - -// OverrideSecretsManager overrides the default Secrets Manager implementation -func OverrideSecretsManager(override secretsmanageriface.SecretsManagerAPI) { - secrets = override -} diff --git a/credentials/credentials_test.go b/credentials/credentials_test.go deleted file mode 100644 index baead03c..00000000 --- a/credentials/credentials_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package credentials - -import ( - "context" - "fmt" - "os" - "testing" - - "github.com/newrelic/newrelic-lambda-extension/config" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/secretsmanager" - "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" - "github.com/stretchr/testify/assert" -) - -func TestGetLicenseKeySecretId(t *testing.T) { - secretId := getLicenseKeySecretId(&config.Configuration{}) - assert.Equal(t, defaultSecretId, secretId) - - var testSecretId = "testSecretName" - var conf = &config.Configuration{LicenseKeySecretId: testSecretId} - secretId = getLicenseKeySecretId(conf) - assert.Equal(t, testSecretId, secretId) -} - -type mockSecretManager struct { - secretsmanageriface.SecretsManagerAPI -} - -func (mockSecretManager) GetSecretValueWithContext(context.Context, *secretsmanager.GetSecretValueInput, ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { - return &secretsmanager.GetSecretValueOutput{ - SecretString: aws.String(`{"LicenseKey": "foo"}`), - }, nil -} - -type mockSecretManagerErr struct { - secretsmanageriface.SecretsManagerAPI -} - -func (mockSecretManagerErr) GetSecretValueWithContext(context.Context, *secretsmanager.GetSecretValueInput, ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { - return nil, fmt.Errorf("Something went wrong") -} - -func TestIsSecretConfigured(t *testing.T) { - OverrideSecretsManager(mockSecretManager{}) - ctx := context.Background() - assert.True(t, IsSecretConfigured(ctx, &config.Configuration{})) - - OverrideSecretsManager(mockSecretManagerErr{}) - assert.False(t, IsSecretConfigured(ctx, &config.Configuration{})) -} - -func TestGetNewRelicLicenseKey(t *testing.T) { - OverrideSecretsManager(mockSecretManager{}) - ctx := context.Background() - lk, err := GetNewRelicLicenseKey(ctx, &config.Configuration{}) - assert.Nil(t, err) - assert.Equal(t, "foo", lk) - - os.Unsetenv("NEW_RELIC_LICENSE_KEY") - OverrideSecretsManager(mockSecretManagerErr{}) - lk, err = GetNewRelicLicenseKey(ctx, &config.Configuration{}) - assert.Error(t, err) - assert.Empty(t, lk) - - os.Setenv("NEW_RELIC_LICENSE_KEY", "foobar") - defer os.Unsetenv("NEW_RELIC_LICENSE_KEY") - lk, err = GetNewRelicLicenseKey(ctx, &config.Configuration{}) - assert.Nil(t, err) - assert.Equal(t, "foobar", lk) -} - -func TestGetNewRelicLicenseKeyConfigValue(t *testing.T) { - licenseKey := "test_value" - ctx := context.Background() - resultKey, err := GetNewRelicLicenseKey(ctx, &config.Configuration{ - LicenseKey: licenseKey, - }) - - assert.Nil(t, err) - assert.Equal(t, licenseKey, resultKey) -} - -func TestDecodeLicenseKey(t *testing.T) { - invalidJson := "invalid json" - decoded, err := decodeLicenseKey(&invalidJson) - assert.Empty(t, decoded) - assert.Error(t, err) -} - -func TestDecodeLicenseKeyValidButWrong(t *testing.T) { - badJson := "{\"some\": \"garbage\"}" - decoded, err := decodeLicenseKey(&badJson) - assert.Empty(t, decoded) - assert.Error(t, err) -} diff --git a/examples/sam/go/go.mod b/examples/sam/go/go.mod new file mode 100644 index 00000000..e6af00d7 --- /dev/null +++ b/examples/sam/go/go.mod @@ -0,0 +1,19 @@ +module example-app + +go 1.19 + +require ( + github.com/newrelic/go-agent/v3 v3.20.3 + github.com/newrelic/go-agent/v3/integrations/nrlambda v1.2.1 +) + +require ( + github.com/aws/aws-lambda-go v1.11.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect + golang.org/x/text v0.3.3 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect + google.golang.org/grpc v1.49.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect +) diff --git a/examples/sam/go/main.go b/examples/sam/go/main.go index 96fd6020..9962e230 100644 --- a/examples/sam/go/main.go +++ b/examples/sam/go/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "github.com/newrelic/go-agent/v3/integrations/nrlambda" "github.com/newrelic/go-agent/v3/newrelic" ) diff --git a/extensionApi/client.go b/extensionApi/client.go new file mode 100644 index 00000000..0e356e15 --- /dev/null +++ b/extensionApi/client.go @@ -0,0 +1,219 @@ +package extensionApi + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + log "github.com/sirupsen/logrus" +) + +// RegisterResponse is the body of the response for /register +type RegisterResponse struct { + FunctionName string `json:"functionName"` + FunctionVersion string `json:"functionVersion"` + Handler string `json:"handler"` +} + +// NextEventResponse is the response for /event/next +type NextEventResponse struct { + EventType EventType `json:"eventType"` + DeadlineMs int64 `json:"deadlineMs"` + RequestID string `json:"requestId"` + InvokedFunctionArn string `json:"invokedFunctionArn"` + Tracing Tracing `json:"tracing"` +} + +// Tracing is part of the response for /event/next +type Tracing struct { + Type string `json:"type"` + Value string `json:"value"` +} + +// StatusResponse is the body of the response for /init/error and /exit/error +type StatusResponse struct { + Status string `json:"status"` +} + +// EventType represents the type of events recieved from /event/next +type EventType string + +const ( + // Function invocation event + Invoke EventType = "INVOKE" + + // Runtime environment shutdown event + Shutdown EventType = "SHUTDOWN" + + extensionNameHeader = "Lambda-Extension-Name" + extensionIdentiferHeader = "Lambda-Extension-Identifier" + extensionErrorType = "Lambda-Extension-Function-Error-Type" +) + +// Client is a simple client for the Lambda Extensions API +type Client struct { + httpClient *http.Client + baseUrl string + ExtensionID string + functionName string +} + +func (e *Client) GetFunctionName() string { + return e.functionName +} + +var l = log.WithFields(log.Fields{"pkg": "extensionApi"}) + +// Returns a Lambda Extensions API client +func NewClient(level log.Level) *Client { + log.SetLevel(level) + baseUrl := fmt.Sprintf("http://%s/2020-01-01/extension", os.Getenv("AWS_LAMBDA_RUNTIME_API")) + return &Client{ + baseUrl: baseUrl, + httpClient: &http.Client{}, + } +} + +// Registers the extension with Extensions API +func (e *Client) Register(ctx context.Context, extensionName string) (string, error) { + const action = "/register" + url := e.baseUrl + action + + l.Debug("[client:Register] Registering using baseURL", e.baseUrl) + reqBody, err := json.Marshal(map[string]interface{}{ + "events": []EventType{Invoke, Shutdown}, + }) + if err != nil { + return "", err + } + + httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBody)) + if err != nil { + return "", err + } + httpReq.Header.Set(extensionNameHeader, extensionName) + + httpRes, err := e.httpClient.Do(httpReq) + if err != nil { + l.Error("[client:Register] Registration failed", err) + return "", err + } + + if httpRes.StatusCode != 200 { + l.Error("[client:Register] Registration failed with statusCode ", httpReq.Response.StatusCode) + return "", fmt.Errorf("registration failed with status %s", httpRes.Status) + } + + defer httpRes.Body.Close() + body, err := io.ReadAll(httpRes.Body) + if err != nil { + return "", err + } + + res := RegisterResponse{} + err = json.Unmarshal(body, &res) + if err != nil { + return "", err + } + + e.functionName = res.FunctionName + e.ExtensionID = httpRes.Header.Get(extensionIdentiferHeader) + l.Debug("[client:Register] Registration success with extensionId ", e.ExtensionID) + return e.ExtensionID, nil +} + +// Blocks while long polling for the next Lambda invoke or shutdown +func (e *Client) NextEvent(ctx context.Context) (*NextEventResponse, error) { + const action = "/event/next" + url := e.baseUrl + action + + httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + httpReq.Header.Set(extensionIdentiferHeader, e.ExtensionID) + httpRes, err := e.httpClient.Do(httpReq) + if err != nil { + return nil, err + } + if httpRes.StatusCode != 200 { + return nil, fmt.Errorf("request failed with status %s", httpRes.Status) + } + defer httpRes.Body.Close() + body, err := io.ReadAll(httpRes.Body) + if err != nil { + return nil, err + } + res := NextEventResponse{} + err = json.Unmarshal(body, &res) + if err != nil { + return nil, err + } + return &res, nil +} + +// Reports an initialization error to the platform. Call it when you registered but failed to initialize +func (e *Client) InitError(errorType string) (*StatusResponse, error) { + const action = "/init/error" + url := e.baseUrl + action + + httpReq, err := http.NewRequest("POST", url, nil) + if err != nil { + return nil, err + } + httpReq.Header.Set(extensionIdentiferHeader, e.ExtensionID) + httpReq.Header.Set(extensionErrorType, errorType) + httpRes, err := e.httpClient.Do(httpReq) + if err != nil { + return nil, err + } + if httpRes.StatusCode != 200 { + return nil, fmt.Errorf("request failed with status %s", httpRes.Status) + } + defer httpRes.Body.Close() + body, err := io.ReadAll(httpRes.Body) + if err != nil { + return nil, err + } + res := StatusResponse{} + err = json.Unmarshal(body, &res) + if err != nil { + return nil, err + } + return &res, nil +} + +// Reports an error to the platform before exiting. Call it when you encounter an unexpected failure +func (e *Client) ExitError(errorType string) (*StatusResponse, error) { + const action = "/exit/error" + url := e.baseUrl + action + + httpReq, err := http.NewRequest("POST", url, nil) + if err != nil { + return nil, err + } + httpReq.Header.Set(extensionIdentiferHeader, e.ExtensionID) + httpReq.Header.Set(extensionErrorType, errorType) + httpRes, err := e.httpClient.Do(httpReq) + if err != nil { + return nil, err + } + if httpRes.StatusCode != 200 { + return nil, fmt.Errorf("request failed with status %s", httpRes.Status) + } + defer httpRes.Body.Close() + body, err := io.ReadAll(httpRes.Body) + if err != nil { + return nil, err + } + res := StatusResponse{} + err = json.Unmarshal(body, &res) + if err != nil { + return nil, err + } + return &res, nil +} diff --git a/go.mod b/go.mod index 97d758f3..1835b0f9 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,20 @@ -module github.com/newrelic/newrelic-lambda-extension +module newrelic-lambda-extension -go 1.14 +go 1.19 require ( - github.com/aws/aws-lambda-go v1.19.1 // indirect - github.com/aws/aws-sdk-go v1.34.21 - github.com/golang/protobuf v1.4.2 // indirect - github.com/google/go-github/v44 v44.1.0 - github.com/google/uuid v1.1.2 - github.com/newrelic/go-agent/v3 v3.9.0 - github.com/newrelic/go-agent/v3/integrations/nrlambda v1.2.0 - github.com/stretchr/testify v1.6.1 - golang.org/x/mod v0.4.1 - google.golang.org/genproto v0.0.0-20200910191746-8ad3c7ee2cd1 // indirect - google.golang.org/grpc v1.32.0 // indirect - google.golang.org/protobuf v1.25.0 // indirect + github.com/aws/aws-sdk-go v1.44.176 + github.com/golang-collections/go-datastructures v0.0.0-20150211160725-59788d5eb259 + github.com/google/uuid v1.3.0 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.9.0 + github.com/stretchr/testify v1.7.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.1.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect ) diff --git a/go.sum b/go.sum index ede4b67c..679a256d 100644 --- a/go.sum +++ b/go.sum @@ -1,145 +1,58 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/aws/aws-lambda-go v1.11.0/go.mod h1:Rr2SMTLeSMKgD45uep9V/NP8tnbCcySgu04cx0k/6cw= -github.com/aws/aws-lambda-go v1.19.1 h1:5iUHbIZ2sG6Yq/J1IN3sWm3+vAB1CWwhI21NffLNuNI= -github.com/aws/aws-lambda-go v1.19.1/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= -github.com/aws/aws-sdk-go v1.34.21 h1:M97FXuiJgDHwD4mXhrIZ7RJ4xXV6uZVPvIC2qb+HfYE= -github.com/aws/aws-sdk-go v1.34.21/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/aws/aws-sdk-go v1.44.176 h1:mxcfI3IWHemX+5fEKt5uqIS/hdbaR7qzGfJYo5UyjJE= +github.com/aws/aws-sdk-go v1.44.176/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v44 v44.1.0 h1:shWPaufgdhr+Ad4eo/pZv9ORTxFpsxPEPEuuXAKIQGA= -github.com/google/go-github/v44 v44.1.0/go.mod h1:iWn00mWcP6PRWHhXm0zuFJ8wbEjE5AGO5D5HXYM4zgw= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/newrelic/go-agent/v3 v3.4.0/go.mod h1:H28zDNUC0U/b7kLoY4EFOhuth10Xu/9dchozUiOseQQ= -github.com/newrelic/go-agent/v3 v3.9.0 h1:5bcTbdk/Up5cIYIkQjCG92Y+uNoett9wmhuz4kPiFlM= -github.com/newrelic/go-agent/v3 v3.9.0/go.mod h1:1A1dssWBwzB7UemzRU6ZVaGDsI+cEn5/bNxI0wiYlIc= -github.com/newrelic/go-agent/v3/integrations/nrlambda v1.2.0 h1:pVLK1gx8YsOoI3EpEZ44HOL5GAnOVNkFx50ZJNKxUBk= -github.com/newrelic/go-agent/v3/integrations/nrlambda v1.2.0/go.mod h1:IZemD4LiJXNBAV652z2x3Awa1Z9Rlx7hEO4OUyqnr+U= +github.com/golang-collections/go-datastructures v0.0.0-20150211160725-59788d5eb259 h1:ZHJ7+IGpuOXtVf6Zk/a3WuHQgkC+vXwaqfUBDFwahtI= +github.com/golang-collections/go-datastructures v0.0.0-20150211160725-59788d5eb259/go.mod h1:9Qcha0gTWLw//0VNka1Cbnjvg3pNKGFdAm7E9sBabxE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200910191746-8ad3c7ee2cd1 h1:Oi/dETbxPPblvoi4hgkzJun62A4dwuBsTM0UcZYpN3U= -google.golang.org/genproto v0.0.0-20200910191746-8ad3c7ee2cd1/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= -google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/lambda/extension/api/api.go b/lambda/extension/api/api.go deleted file mode 100644 index 9e1c77a8..00000000 --- a/lambda/extension/api/api.go +++ /dev/null @@ -1,116 +0,0 @@ -// Package api contains types and constants for interacting with the AWS Lambda Extension API. -package api - -import ( - "fmt" - "time" -) - -// LifecycleEvent represents lifecycle events that the extension can express interest in -type LifecycleEvent string -type ShutdownReason string -type LogEventType string - -const ( - Invoke LifecycleEvent = "INVOKE" - Shutdown LifecycleEvent = "SHUTDOWN" - - Spindown ShutdownReason = "spindown" - Timeout ShutdownReason = "timeout" - Failure ShutdownReason = "failure" - - Platform LogEventType = "platform" - Function = "function" - Extension = "extension" - - Version string = "2020-01-01" - LogsApiVersion = "2020-08-15" - - LambdaHostPortEnvVar = "AWS_LAMBDA_RUNTIME_API" - - ExtensionNameHeader = "Lambda-Extension-Name" - ExtensionIdHeader = "Lambda-Extension-Identifier" - ExtensionErrorTypeHeader = "Lambda-Extension-Function-Error-Type" - - LogBufferDefaultBytes uint32 = 1024 * 1024 - LogBufferDefaultItems uint32 = 10_000 - LogBufferDefaultTimeout uint32 = 500 -) - -type InvocationEvent struct { - // Either INVOKE or SHUTDOWN. - EventType LifecycleEvent `json:"eventType"` - // The instant that the invocation times out, as epoch milliseconds - DeadlineMs int64 `json:"deadlineMs"` - // The AWS Request ID, for INVOKE events. - RequestID string `json:"requestId"` - // The ARN of the function being invoked, for INVOKE events. - InvokedFunctionARN string `json:"invokedFunctionArn"` - // XRay trace ID, for INVOKE events. - Tracing map[string]string `json:"tracing"` - // The reason for termination, if this is a shutdown event - ShutdownReason ShutdownReason `json:"shutdownReason"` -} - -type RegistrationRequest struct { - Events []LifecycleEvent `json:"events"` -} - -type RegistrationResponse struct { - FunctionName string `json:"functionName"` - FunctionVersion string `json:"functionVersion"` - Handler string `json:"handler"` -} - -type LogSubscription struct { - Buffering BufferingCfg `json:"buffering"` - Destination DestinationCfg `json:"destination"` - Types []LogEventType `json:"types"` -} - -func NewLogSubscription(bufferingCfg BufferingCfg, destinationCfg DestinationCfg, types []LogEventType) *LogSubscription { - return &LogSubscription{ - Buffering: bufferingCfg, - Destination: destinationCfg, - Types: types, - } -} - -func DefaultLogSubscription(types []LogEventType, port uint16) *LogSubscription { - endpoint := formatLogsEndpoint(port) - - return NewLogSubscription( - BufferingCfg{ - MaxBytes: LogBufferDefaultBytes, - MaxItems: LogBufferDefaultItems, - TimeoutMs: LogBufferDefaultTimeout, - }, - DestinationCfg{ - URI: endpoint, - Protocol: "HTTP", - }, - types, - ) -} - -func formatLogsEndpoint(port uint16) string { - return fmt.Sprintf("http://sandbox:%d", port) -} - -type BufferingCfg struct { - MaxBytes uint32 `json:"maxBytes"` - MaxItems uint32 `json:"maxItems"` - TimeoutMs uint32 `json:"timeoutMs"` -} - -type DestinationCfg struct { - URI string `json:"URI"` - Protocol string `json:"protocol"` - //Port uint16 `json:"port"` //Not used by us -} - -type LogEvent struct { - Time time.Time `json:"time"` - Type string `json:"type"` - Record interface{} `json:"record"` -} diff --git a/lambda/extension/api/api_test.go b/lambda/extension/api/api_test.go deleted file mode 100644 index c539b567..00000000 --- a/lambda/extension/api/api_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package api - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_formatLogsEndpoint(t *testing.T) { - endpoint := formatLogsEndpoint(1234) - - assert.Equal(t, "http://sandbox:1234", endpoint) -} - -func Test_DefaultLogSubscription(t *testing.T) { - types := []LogEventType{Platform} - sub := DefaultLogSubscription(types, 2345) - - assert.Equal(t, LogBufferDefaultBytes, sub.Buffering.MaxBytes) - assert.Equal(t, LogBufferDefaultItems, sub.Buffering.MaxItems) - assert.Equal(t, LogBufferDefaultTimeout, sub.Buffering.TimeoutMs) - - assert.Equal(t, "http://sandbox:2345", sub.Destination.URI) - assert.Equal(t, "HTTP", sub.Destination.Protocol) - assert.Equal(t, types, sub.Types) -} diff --git a/lambda/extension/client/client.go b/lambda/extension/client/client.go deleted file mode 100644 index 61d4b6db..00000000 --- a/lambda/extension/client/client.go +++ /dev/null @@ -1,274 +0,0 @@ -// Package client is a generic client for the AWS Lambda Extension API. -// The API's lifecycle begins with execution of the extension binary, which is expected to register. -// The extension then makes blocking requests for the next event. The response to the next event request -// is either a notification of the next event, or a shutdown notification. -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "os" - "path/filepath" - - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/util" -) - -// InvocationClient is used to poll for invocation events. It is produced as a result of successful -// registration. The zero value is not usable. -type InvocationClient struct { - version string - baseUrl string - httpClient http.Client - extensionId string -} - -// RegistrationClient is used to register, and acquire an InvocationClient. The zero value is not usable. -type RegistrationClient struct { - extensionName string - version string - baseUrl string - httpClient http.Client -} - -// Constructs a new RegistrationClient. This is the entry point. -func New(httpClient http.Client) *RegistrationClient { - exePath, err := os.Executable() - if err != nil { - util.Fatal(err) - } - - exeName := filepath.Base(exePath) - - return &RegistrationClient{ - extensionName: exeName, - version: api.Version, - baseUrl: os.Getenv(api.LambdaHostPortEnvVar), - httpClient: httpClient, - } -} - -// getRegisterURL returns the Lambda Extension register URL -func (rc *RegistrationClient) getRegisterURL() string { - return fmt.Sprintf("http://%s/%s/extension/register", rc.baseUrl, rc.version) -} - -// RegisterDefault registers for Invoke and Shutdown events, with no configuration parameters. -func (rc *RegistrationClient) RegisterDefault(ctx context.Context) (*InvocationClient, *api.RegistrationResponse, error) { - defaultEvents := []api.LifecycleEvent{api.Invoke, api.Shutdown} - defaultRequest := api.RegistrationRequest{Events: defaultEvents} - return rc.Register(ctx, defaultRequest) -} - -// Register registers, with custom registration parameters. -func (rc *RegistrationClient) Register(ctx context.Context, registrationRequest api.RegistrationRequest) (*InvocationClient, *api.RegistrationResponse, error) { - registrationRequestJson, err := json.Marshal(registrationRequest) - if err != nil { - return nil, nil, fmt.Errorf("error occurred while marshaling registration request %s", err) - } - - req, err := http.NewRequestWithContext(ctx, "POST", rc.getRegisterURL(), bytes.NewBuffer(registrationRequestJson)) - if err != nil { - return nil, nil, fmt.Errorf("error occurred while creating registration request %s", err) - } - - req.Header.Set(api.ExtensionNameHeader, rc.extensionName) - - res, err := rc.httpClient.Do(req) - if err != nil { - return nil, nil, fmt.Errorf("error occurred while making registration request %s", err) - } - - defer util.Close(res.Body) - - if res.StatusCode == http.StatusInternalServerError { - util.Panic("error occurred while making registration request: ", res.Status) - } - - if res.StatusCode != http.StatusOK { - return nil, nil, fmt.Errorf("error occurred while making registration request: %s", res.Status) - } - - bodyBytes, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, nil, err - } - - util.Debugf("Registration response: %s", bodyBytes) - - var registrationResponse api.RegistrationResponse - err = json.Unmarshal(bodyBytes, ®istrationResponse) - if err != nil { - return nil, nil, err - } - - id, exists := res.Header[api.ExtensionIdHeader] - if !exists { - return nil, nil, fmt.Errorf("missing extension identifier. Response body %s", bodyBytes) - } - - invocationClient := InvocationClient{rc.version, rc.baseUrl, rc.httpClient, id[0]} - return &invocationClient, ®istrationResponse, nil -} - -// getNextEventURL returns the Lambda Extension next event URL -func (ic *InvocationClient) getNextEventURL() string { - return fmt.Sprintf("http://%s/%s/extension/event/next", ic.baseUrl, ic.version) -} - -// getInitErrorURL returns the Lambda Extension initialization error URL -func (ic *InvocationClient) getInitErrorURL() string { - return fmt.Sprintf("http://%s/%s/extension/init/error", ic.baseUrl, ic.version) -} - -// getExitErrorURL returns the Lambda exit error URL -func (ic *InvocationClient) getExitErrorURL() string { - return fmt.Sprintf("http://%s/%s/extension/exit/error", ic.baseUrl, ic.version) -} - -// getLogRegistrationURL returns the Lambda Log Registration URL -func (ic *InvocationClient) getLogRegistrationURL() string { - return fmt.Sprintf("http://%s/%s/logs", ic.baseUrl, api.LogsApiVersion) -} - -// LogRegister registers for log events -func (ic *InvocationClient) LogRegister(ctx context.Context, subscriptionRequest *api.LogSubscription) error { - subscriptionRequestJson, err := json.Marshal(subscriptionRequest) - if err != nil { - return fmt.Errorf("error occurred while marshaling subscription request %s", err) - } - - util.Debugln("Log registration with request ", string(subscriptionRequestJson)) - - req, err := http.NewRequestWithContext(ctx, "PUT", ic.getLogRegistrationURL(), bytes.NewBuffer(subscriptionRequestJson)) - if err != nil { - return fmt.Errorf("error occurred while creating subscription request %s", err) - } - - req.Header.Set(api.ExtensionIdHeader, ic.extensionId) - - res, err := ic.httpClient.Do(req) - if err != nil { - return fmt.Errorf("error occurred while making log subscription request %s", err) - } - - defer util.Close(res.Body) - - if res.StatusCode == http.StatusInternalServerError { - util.Panic("error occurred while making log subscription request: ", res.Status) - } - - if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusAccepted { - return fmt.Errorf("error occurred while making log subscription request: %s", res.Status) - } - - responseBody, err := ioutil.ReadAll(res.Body) - if err != nil { - return err - } - - util.Debugln("Registered for logs. Got response code ", res.StatusCode, string(responseBody)) - - return nil -} - -// NextEvent awaits the next event. -func (ic *InvocationClient) NextEvent(ctx context.Context) (*api.InvocationEvent, error) { - req, err := http.NewRequestWithContext(ctx, "GET", ic.getNextEventURL(), nil) - if err != nil { - return nil, fmt.Errorf("error occurred when creating next request %s", err) - } - - req.Header.Set(api.ExtensionIdHeader, ic.extensionId) - - res, err := ic.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("error occurred when calling extension/event/next %s", err) - } - - defer util.Close(res.Body) - - if res.StatusCode == http.StatusInternalServerError { - util.Panic("error occurred when calling extension/event/next: ", res.Status) - } - - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("error occurred when calling extension/event/next: %s", res.Status) - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("error occurred while reading extension/event/next response body %s", err) - } - - var event api.InvocationEvent - err = json.Unmarshal(body, &event) - if err != nil { - return nil, fmt.Errorf("error occurred while unmarshaling extension/event/next response body %s", err) - } - - return &event, nil -} - -// InitError sends an initialization error to the lambda platform -func (ic *InvocationClient) InitError(ctx context.Context, errorEnum string, initError error) error { - errorBuf := bytes.NewBufferString(initError.Error()) - - req, err := http.NewRequestWithContext(ctx, "POST", ic.getInitErrorURL(), errorBuf) - if err != nil { - return fmt.Errorf("error occurred when creating init error request %s", err) - } - - req.Header.Set(api.ExtensionIdHeader, ic.extensionId) - req.Header.Set(api.ExtensionErrorTypeHeader, errorEnum) - - res, err := ic.httpClient.Do(req) - if err != nil { - return fmt.Errorf("error occurred when calling extension/init/error %s", err) - } - - defer util.Close(res.Body) - - if res.StatusCode == http.StatusInternalServerError { - util.Panic("error occurred while making init error request: ", res.Status) - } - - if res.StatusCode != http.StatusAccepted { - return fmt.Errorf("error occurred while making init error request: %s", res.Status) - } - - return nil -} - -// ExitError sends an exit error to the lambda platform -func (ic *InvocationClient) ExitError(ctx context.Context, errorEnum string, exitError error) error { - errorBuf := bytes.NewBufferString(exitError.Error()) - req, err := http.NewRequestWithContext(ctx, "POST", ic.getExitErrorURL(), errorBuf) - if err != nil { - return fmt.Errorf("error occurred when creating exit error request %s", err) - } - - req.Header.Set(api.ExtensionIdHeader, ic.extensionId) - req.Header.Set(api.ExtensionErrorTypeHeader, errorEnum) - - res, err := ic.httpClient.Do(req) - if err != nil { - return fmt.Errorf("error occurred when calling extension/exit/error %s", err) - } - - defer util.Close(res.Body) - - if res.StatusCode == http.StatusInternalServerError { - util.Panic("error occurred while making exit error request: ", res.Status) - } - - if res.StatusCode != http.StatusAccepted { - return fmt.Errorf("error occurred while making exit error request: %s", res.Status) - } - - return nil -} diff --git a/lambda/extension/client/client_test.go b/lambda/extension/client/client_test.go deleted file mode 100644 index c4e49c27..00000000 --- a/lambda/extension/client/client_test.go +++ /dev/null @@ -1,448 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/util" - - "github.com/stretchr/testify/assert" -) - -var exePath, _ = os.Executable() -var exeName = filepath.Base(exePath) - -func TestNew(t *testing.T) { - _ = os.Setenv(api.LambdaHostPortEnvVar, "127.0.0.1:8123") - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - client := New(http.Client{}) - - assert.Equal(t, exeName, client.extensionName) -} - -func TestRegistrationClient_GetRegisterURL(t *testing.T) { - _ = os.Setenv(api.LambdaHostPortEnvVar, "127.0.0.1:8123") - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - client := New(http.Client{}) - - assert.Equal(t, "http://127.0.0.1:8123/2020-01-01/extension/register", client.getRegisterURL()) -} - -func TestRegistrationClient_RegisterDefault(t *testing.T) { - rc := RegistrationClient{} - ctx := context.Background() - ic, res, err := rc.RegisterDefault(ctx) - assert.Nil(t, ic) - assert.Nil(t, res) - assert.Error(t, err) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, r.Method, http.MethodPost) - assert.NotEmpty(t, r.Header.Get(api.ExtensionNameHeader)) - - reqBytes, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - defer util.Close(r.Body) - - assert.NotEmpty(t, reqBytes) - - var reqData api.RegistrationRequest - assert.NoError(t, json.Unmarshal(reqBytes, &reqData)) - assert.Equal(t, []api.LifecycleEvent{api.Invoke, api.Shutdown}, reqData.Events) - - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(200) - respBytes, _ := json.Marshal(api.RegistrationResponse{}) - _, _ = w.Write(respBytes) - })) - - defer srv.Close() - - url := srv.URL[7:] - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - client := New(*srv.Client()) - invocationClient, rr, err := client.RegisterDefault(ctx) - - assert.NoError(t, err) - assert.Equal(t, "test-ext-id", invocationClient.extensionId) - assert.NotNil(t, rr) - assert.NotEmpty(t, invocationClient.getInitErrorURL()) - assert.NotEmpty(t, invocationClient.getExitErrorURL()) - assert.NotEmpty(t, invocationClient.getLogRegistrationURL()) -} - -func TestRegistrationClient_RegisterError(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(400) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - client := New(*srv.Client()) - ctx := context.Background() - ic, rr, err := client.RegisterDefault(ctx) - - assert.Nil(t, ic) - assert.Nil(t, rr) - assert.Error(t, err) -} - -func TestRegistrationClient_RegisterPanic(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(500) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - client := New(*srv.Client()) - ctx := context.Background() - - assert.Panics(t, func() { - client.RegisterDefault(ctx) - }) -} - -func TestInvocationClient_LogRegister(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, r.Method, http.MethodPut) - - assert.NotEmpty(t, r.Header.Get(api.ExtensionIdHeader)) - defer util.Close(r.Body) - - w.WriteHeader(200) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - eventTypes := []api.LogEventType{api.Platform} - subscriptionRequest := api.DefaultLogSubscription(eventTypes, 12345) - - ctx := context.Background() - err := client.LogRegister(ctx, subscriptionRequest) - - assert.NoError(t, err) -} - -func TestInvocationClient_LogRegisterError(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - w.WriteHeader(400) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - eventTypes := []api.LogEventType{api.Platform} - subscriptionRequest := api.DefaultLogSubscription(eventTypes, 12345) - - ctx := context.Background() - err := client.LogRegister(ctx, subscriptionRequest) - - assert.Error(t, err) -} - -func TestInvocationClient_LogRegisterEPanic(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - w.WriteHeader(500) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - eventTypes := []api.LogEventType{api.Platform} - subscriptionRequest := api.DefaultLogSubscription(eventTypes, 12345) - - ctx := context.Background() - assert.Panics(t, func() { - client.LogRegister(ctx, subscriptionRequest) - }) -} - -func TestInvocationClient_InitError(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, r.Method, http.MethodPost) - - assert.NotEmpty(t, r.Header.Get(api.ExtensionIdHeader)) - defer util.Close(r.Body) - - w.WriteHeader(202) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - ctx := context.Background() - err := client.InitError(ctx, "foo.bar", errors.New("something went wrong")) - - assert.NoError(t, err) -} - -func TestInvocationClient_InitErrorError(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - w.WriteHeader(400) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - ctx := context.Background() - err := client.InitError(ctx, "foo.bar", errors.New("something went wrong")) - - assert.Error(t, err) -} - -func TestInvocationClient_InitErrorPanic(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - w.WriteHeader(500) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - ctx := context.Background() - assert.Panics(t, func() { - client.InitError(ctx, "foo.bar", errors.New("something went wrong")) - }) -} - -func TestInvocationClient_ExitError(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, r.Method, http.MethodPost) - - assert.NotEmpty(t, r.Header.Get(api.ExtensionIdHeader)) - defer util.Close(r.Body) - - w.WriteHeader(202) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - ctx := context.Background() - err := client.ExitError(ctx, "foo.bar", errors.New("something went wrong")) - - assert.NoError(t, err) -} - -func TestInvocationClient_ExitErrorError(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - w.WriteHeader(400) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - ctx := context.Background() - err := client.ExitError(ctx, "foo.bar", errors.New("something went wrong")) - - assert.Error(t, err) -} - -func TestInvocationClient_ExitErrorPanic(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - w.WriteHeader(500) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - ctx := context.Background() - assert.Panics(t, func() { - client.ExitError(ctx, "foo.bar", errors.New("something went wrong")) - }) -} - -func TestInvocationClient_NextEvent(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, r.Method, http.MethodGet) - - assert.NotEmpty(t, r.Header.Get(api.ExtensionIdHeader)) - defer util.Close(r.Body) - - w.WriteHeader(200) - respBytes, _ := json.Marshal(api.InvocationEvent{ - EventType: api.Invoke, - DeadlineMs: 1234, - RequestID: "5678", - InvokedFunctionARN: "arn:aws:test", - Tracing: nil, - }) - _, _ = w.Write(respBytes) - })) - - defer srv.Close() - - url := srv.URL[7:] - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - ctx := context.Background() - invocationEvent, err := client.NextEvent(ctx) - - assert.NoError(t, err) - assert.NotNil(t, invocationEvent) -} - -func TestInvocationClient_NextEventError(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - w.WriteHeader(400) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - ctx := context.Background() - event, err := client.NextEvent(ctx) - - assert.Error(t, err) - assert.Nil(t, event) -} - -func TestInvocationClient_NextEventPanic(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - w.WriteHeader(500) - _, _ = w.Write(nil) - })) - defer srv.Close() - - url := srv.URL[7:] - - client := InvocationClient{ - version: api.Version, - baseUrl: url, - httpClient: *srv.Client(), - extensionId: "test-ext-id", - } - - ctx := context.Background() - assert.Panics(t, func() { - client.NextEvent(ctx) - }) -} diff --git a/lambda/logserver/logserver.go b/lambda/logserver/logserver.go deleted file mode 100644 index cc2ae926..00000000 --- a/lambda/logserver/logserver.go +++ /dev/null @@ -1,221 +0,0 @@ -package logserver - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net" - "net/http" - "regexp" - "strconv" - "sync" - "time" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/util" -) - -const ( - platformLogBufferSize = 100 -) - -type LogLine struct { - Time time.Time - RequestID string - Content []byte -} - -type LogServer struct { - listenString string - server *http.Server - platformLogChan chan LogLine - functionLogChan chan []LogLine - lastRequestId string - lastRequestIdLock *sync.Mutex -} - -func (ls *LogServer) Port() uint16 { - _, portStr, _ := net.SplitHostPort(ls.listenString) - port, _ := strconv.ParseUint(portStr, 10, 16) - return uint16(port) -} - -func (ls *LogServer) Close() error { - // Pause briefly to allow final platform logs to arrive - time.Sleep(200 * time.Millisecond) - - ret := ls.server.Close() - close(ls.platformLogChan) - close(ls.functionLogChan) - return ret -} - -func (ls *LogServer) PollPlatformChannel() []LogLine { - var ret []LogLine - - for { - select { - case report, more := <-ls.platformLogChan: - if more { - ret = append(ret, report) - } else { - return ret - } - default: - return ret - } - } -} - -func (ls *LogServer) AwaitFunctionLogs() ([]LogLine, bool) { - ll, more := <-ls.functionLogChan - return ll, more -} - -func formatReport(metrics map[string]interface{}) string { - ret := "" - - if val, ok := metrics["durationMs"]; ok { - ret += fmt.Sprintf("\tDuration: %.2f ms", val) - } - - if val, ok := metrics["billedDurationMs"]; ok { - ret += fmt.Sprintf("\tBilled Duration: %.0f ms", val) - } - - if val, ok := metrics["memorySizeMB"]; ok { - ret += fmt.Sprintf("\tMemory Size: %.0f MB", val) - } - - if val, ok := metrics["maxMemoryUsedMB"]; ok { - ret += fmt.Sprintf("\tMax Memory Used: %.0f MB", val) - } - - if val, ok := metrics["initDurationMs"]; ok { - ret += fmt.Sprintf("\tInit Duration: %.2f ms", val) - } - - return ret -} - -var reportStringRegExp, _ = regexp.Compile("RequestId: ([a-fA-F0-9-]+)(.*)") - -func (ls *LogServer) handler(res http.ResponseWriter, req *http.Request) { - defer util.Close(req.Body) - - bodyBytes, err := ioutil.ReadAll(req.Body) - if err != nil { - util.Logf("Error processing log request: %v", err) - } - - var logEvents []api.LogEvent - err = json.Unmarshal(bodyBytes, &logEvents) - if err != nil { - util.Logf("Error parsing log payload: %v", err) - } - - var functionLogs []LogLine - - for _, event := range logEvents { - switch event.Type { - case "platform.start": - ls.lastRequestIdLock.Lock() - switch event.Record.(type) { - case map[string]interface{}: - ls.lastRequestId = event.Record.(map[string]interface{})["requestId"].(string) - case string: - recordString := event.Record.(string) - results := reportStringRegExp.FindStringSubmatch(recordString) - if len(results) > 1 { - ls.lastRequestId = results[1] - } - } - ls.lastRequestIdLock.Unlock() - case "platform.report": - metricString := "" - requestId := "" - switch event.Record.(type) { - case map[string]interface{}: - record := event.Record.(map[string]interface{}) - metrics := record["metrics"].(map[string]interface{}) - metricString = formatReport(metrics) - requestId = record["requestId"].(string) - case string: - recordString := event.Record.(string) - results := reportStringRegExp.FindStringSubmatch(recordString) - if len(results) > 1 { - requestId = results[1] - if len(results) > 2 { - metricString = results[2] - } - } else { - util.Debugf("Unknown platform log: %s", recordString) - } - } - - reportStr := fmt.Sprintf( - "REPORT RequestId: %v%s", - requestId, - metricString, - ) - reportLine := LogLine{ - Time: event.Time, - RequestID: requestId, - Content: []byte(reportStr), - } - ls.platformLogChan <- reportLine - case "platform.logsDropped": - util.Logf("Platform dropped logs: %v", event.Record) - case "function": - record := event.Record.(string) - ls.lastRequestIdLock.Lock() - functionLogs = append(functionLogs, LogLine{ - Time: event.Time, - RequestID: ls.lastRequestId, - Content: []byte(record), - }) - ls.lastRequestIdLock.Unlock() - default: - //util.Debugln("Ignored log event of type ", event.Type, string(bodyBytes)) - } - } - - if len(functionLogs) > 0 { - ls.functionLogChan <- functionLogs - } - - _, _ = res.Write(nil) -} - -func Start(conf *config.Configuration) (*LogServer, error) { - return startInternal(conf.LogServerHost) -} - -func startInternal(host string) (*LogServer, error) { - listener, err := net.Listen("tcp", host+":") - if err != nil { - return nil, err - } - - server := &http.Server{} - - logServer := &LogServer{ - listenString: listener.Addr().String(), - server: server, - platformLogChan: make(chan LogLine, platformLogBufferSize), - functionLogChan: make(chan []LogLine), - lastRequestIdLock: &sync.Mutex{}, - } - - mux := http.NewServeMux() - mux.HandleFunc("/", logServer.handler) - server.Handler = mux - - go func() { - util.Logln("Starting log server.") - util.Logf("Log server terminating: %v\n", server.Serve(listener)) - }() - - return logServer, nil -} diff --git a/lambda/logserver/logserver_test.go b/lambda/logserver/logserver_test.go deleted file mode 100644 index dd38754f..00000000 --- a/lambda/logserver/logserver_test.go +++ /dev/null @@ -1,205 +0,0 @@ -//go:build !race -// +build !race - -package logserver - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "testing" - "time" - - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/stretchr/testify/assert" -) - -func TestLogServer(t *testing.T) { - logs, err := startInternal("localhost") - assert.NoError(t, err) - - testEvents := []api.LogEvent{ - { - Time: time.Now(), - Type: "platform.report", - Record: map[string]interface{}{ - "metrics": map[string]float64{ - "durationMs": 25.3, - "billedDurationMs": 100.0, - "memorySizeMB": 128.0, - "maxMemoryUsedMB": 73.5, - "initDurationMs": 202.0, - }, - "requestId": "testRequestId", - }, - }, - } - - testEventBytes, err := json.Marshal(testEvents) - assert.NoError(t, err) - - realEndpoint := fmt.Sprintf("http://localhost:%d", logs.Port()) - req, err := http.NewRequest("POST", realEndpoint, bytes.NewBuffer(testEventBytes)) - assert.NoError(t, err) - - client := http.Client{} - res, err := client.Do(req) - - assert.NoError(t, err) - assert.Equal(t, 200, res.StatusCode) - assert.Equal(t, http.NoBody, res.Body) - - logLines := logs.PollPlatformChannel() - - assert.Equal(t, 1, len(logLines)) - assert.Equal(t, "REPORT RequestId: testRequestId\tDuration: 25.30 ms\tBilled Duration: 100 ms\tMemory Size: 128 MB\tMax Memory Used: 74 MB\tInit Duration: 202.00 ms", string(logLines[0].Content)) - - assert.Nil(t, logs.Close()) -} - -func TestFunctionLogs(t *testing.T) { - logs, err := startInternal("localhost") - assert.NoError(t, err) - - testEvents := []api.LogEvent{ - { - Time: time.Now().Add(-100 * time.Millisecond), - Type: "platform.start", - Record: map[string]interface{}{ - "requestId": "testRequestId", - }, - }, - { - Time: time.Now().Add(-50 * time.Millisecond), - Type: "function", - Record: "log line 1", - }, - } - - testEventBytes, err := json.Marshal(testEvents) - assert.NoError(t, err) - - realEndpoint := fmt.Sprintf("http://localhost:%d", logs.Port()) - req, err := http.NewRequest("POST", realEndpoint, bytes.NewBuffer(testEventBytes)) - assert.NoError(t, err) - - client := http.Client{} - go func() { - res, err := client.Do(req) - - assert.NoError(t, err) - assert.Equal(t, 200, res.StatusCode) - assert.Equal(t, http.NoBody, res.Body) - }() - - logLines, _ := logs.AwaitFunctionLogs() - - assert.Equal(t, 1, len(logLines)) - assert.Equal(t, "log line 1", string(logLines[0].Content)) - assert.Equal(t, "testRequestId", logLines[0].RequestID) - - testEvents2 := []api.LogEvent{ - { - Time: time.Now().Add(500 * time.Millisecond), - Type: "function", - Record: "log line 2", - }, - } - - testEventBytes, err = json.Marshal(testEvents2) - assert.NoError(t, err) - - req, err = http.NewRequest("POST", realEndpoint, bytes.NewBuffer(testEventBytes)) - assert.NoError(t, err) - - go func() { - res, err := client.Do(req) - assert.NoError(t, err) - assert.Equal(t, 200, res.StatusCode) - assert.Equal(t, http.NoBody, res.Body) - }() - - logLines2, _ := logs.AwaitFunctionLogs() - - assert.Equal(t, 1, len(logLines2)) - assert.Equal(t, "log line 2", string(logLines2[0].Content)) - assert.Equal(t, "testRequestId", logLines2[0].RequestID) - - testRequestId := "abcdef01-a2b3-4321-cd89-0123456789ab" - - testEvents3 := []api.LogEvent{ - { - Time: time.Now().Add(600 * time.Millisecond), - Type: "platform.start", - Record: "RequestId: " + testRequestId, - }, - { - Time: time.Now().Add(700 * time.Millisecond), - Type: "function", - Record: "log line 3, for testing start line record as string", - }, - } - - testEventBytes, err = json.Marshal(testEvents3) - assert.NoError(t, err) - - req, err = http.NewRequest("POST", realEndpoint, bytes.NewBuffer(testEventBytes)) - assert.NoError(t, err) - - go func() { - res, err := client.Do(req) - assert.NoError(t, err) - assert.Equal(t, 200, res.StatusCode) - assert.Equal(t, http.NoBody, res.Body) - }() - - logLines3, _ := logs.AwaitFunctionLogs() - - assert.Equal(t, 1, len(logLines3)) - assert.Equal(t, "log line 3, for testing start line record as string", string(logLines3[0].Content)) - assert.Equal(t, testRequestId, logLines3[0].RequestID) - - platformMetricString := "REPORT RequestId: " + testRequestId + "\tDuration: 25.30 ms\tBilled Duration: 100 ms\tMemory Size: 128 MB\tMax Memory Used: 74 MB\tInit Duration: 202.00 ms" - - testEvents4 := []api.LogEvent{ - { - Time: time.Now().Add(800 * time.Millisecond), - Type: "platform.report", - Record: platformMetricString, - }, - { - Time: time.Now().Add(900 * time.Millisecond), - Type: "function", - Record: "log line 4, testing platform metrics as string", - }, - } - - testEventBytes, err = json.Marshal(testEvents4) - assert.NoError(t, err) - - req, err = http.NewRequest("POST", realEndpoint, bytes.NewBuffer(testEventBytes)) - assert.NoError(t, err) - - go func() { - res, err := client.Do(req) - assert.NoError(t, err) - assert.Equal(t, 200, res.StatusCode) - assert.Equal(t, http.NoBody, res.Body) - }() - - logLines4, _ := logs.AwaitFunctionLogs() - - assert.Equal(t, 1, len(logLines4)) - assert.Equal(t, "log line 4, testing platform metrics as string", string(logLines4[0].Content)) - assert.Equal(t, testRequestId, logLines4[0].RequestID) - - assert.Nil(t, logs.Close()) -} - -func TestLogServerStart(t *testing.T) { - logs, err := Start(&config.Configuration{LogServerHost: "localhost"}) - assert.NoError(t, err) - assert.Nil(t, logs.Close()) -} diff --git a/main.go b/main.go index 8e0acc95..de247373 100644 --- a/main.go +++ b/main.go @@ -1,348 +1,159 @@ package main +/* +Notes: +- Because of the asynchronous nature of the system, it is possible that telemetry for one invoke will be + processed during the next invoke slice. Likewise, it is possible that telemetry for the last invoke will + be processed during the SHUTDOWN event. +*/ + import ( "context" - "encoding/base64" - "fmt" - "net/http" - "os" - "os/signal" + "newrelic-lambda-extension/agentTelemetry" + "newrelic-lambda-extension/config" + "newrelic-lambda-extension/extensionApi" + "newrelic-lambda-extension/telemetryApi" + "newrelic-lambda-extension/util" "sync" - "syscall" "time" - "github.com/newrelic/newrelic-lambda-extension/checks" - "github.com/newrelic/newrelic-lambda-extension/lambda/logserver" - "github.com/newrelic/newrelic-lambda-extension/util" + "os" + "os/signal" + "syscall" - "github.com/newrelic/newrelic-lambda-extension/config" - "github.com/newrelic/newrelic-lambda-extension/credentials" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/client" - "github.com/newrelic/newrelic-lambda-extension/telemetry" + "github.com/golang-collections/go-datastructures/queue" + log "github.com/sirupsen/logrus" ) var ( - invokedFunctionARN string - lastEventStart time.Time - lastRequestId string - rootCtx context.Context + l = log.WithFields(log.Fields{"pkg": "main"}) ) -func init() { - rootCtx = context.Background() -} - func main() { - extensionStartup := time.Now() + l.Infof("[main] Starting the New Relic Telemetry API extension version %s", util.Version) + ctx, cancel := context.WithCancel(context.Background()) - ctx, cancel := context.WithCancel(rootCtx) - defer cancel() + // Handle User Configured Settings + conf := config.GetConfig() + log.SetLevel(conf.LogLevel) + log.SetFormatter(&log.TextFormatter{ + DisableTimestamp: true, + }) - // exit cleanly on SIGTERM or SIGINT sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT) go func() { + // ctrl + c escape s := <-sigs cancel() - util.Logf("Received %v Exiting", s) + l.Info("[main] Received", s) + l.Info("[main] Exiting") }() - // Allow extension to be interrupted with CTRL-C - ctrlCChan := make(chan os.Signal, 1) - signal.Notify(ctrlCChan, os.Interrupt) - go func() { - for range ctrlCChan { - cancel() - util.Fatal("Exiting...") - } - }() - - // Parse various env vars for our config - conf := config.ConfigurationFromEnvironment() - - // Optionally enable debug logging, disabled by default - util.ConfigLogger(conf.LogsEnabled, conf.LogLevel == config.DebugLogLevel) - - // Extensions must register - registrationClient := client.New(http.Client{}) - - regReq := api.RegistrationRequest{ - Events: []api.LifecycleEvent{api.Invoke, api.Shutdown}, - } - - invocationClient, registrationResponse, err := registrationClient.Register(ctx, regReq) - if err != nil { - util.Panic(err) - } - - // If extension disabled, go into no op mode - if !conf.ExtensionEnabled { - util.Logln("Extension telemetry processing disabled") - noopLoop(ctx, invocationClient) - return - } - - // Attempt to find the license key for telemetry sending - licenseKey, err := credentials.GetNewRelicLicenseKey(ctx, conf) + // Step 1 - Register the extension with Extensions API + l.Debug("[main] Registering extension") + extensionApiClient := extensionApi.NewClient(conf.LogLevel) + extensionId, err := extensionApiClient.Register(ctx, conf.ExtensionName) if err != nil { - util.Logln("Failed to retrieve New Relic license key", err) - // We fail open; telemetry will go to CloudWatch instead - noopLoop(ctx, invocationClient) - return + l.Fatal(err) } + l.Debug("[main] Registation success with extensionId", extensionId) - // Set up the telemetry buffer - batch := telemetry.NewBatch(int64(conf.RipeMillis), int64(conf.RotMillis), conf.CollectTraceID) - - // Start the Logs API server, and register it - logServer, err := logserver.Start(conf) - if err != nil { - err2 := invocationClient.InitError(ctx, "logServer.start", err) - if err2 != nil { - util.Logln(err2) - } - util.Panic("Failed to start logs HTTP server", err) + // Get New Relic License Key and Lambda Function Name + conf.ExtensionName = extensionApiClient.GetFunctionName() + if len(conf.ExtensionName) > telemetryApi.MaxAttributeValueLen { + conf.ExtensionName = conf.ExtensionName[:telemetryApi.MaxAttributeValueLen] } - eventTypes := []api.LogEventType{api.Platform} - if conf.SendFunctionLogs { - eventTypes = append(eventTypes, api.Function) - } - subscriptionRequest := api.DefaultLogSubscription(eventTypes, logServer.Port()) - err = invocationClient.LogRegister(ctx, subscriptionRequest) - if err != nil { - err2 := invocationClient.InitError(ctx, "logServer.register", err) - if err2 != nil { - util.Logln(err2) - } - util.Panic("Failed to register with Logs API", err) - } + conf.LicenseKey = telemetryApi.GetNewRelicLicenseKey(ctx) + l.Tracef("Final Config: %+v", conf) - // Init the telemetry sending client - telemetryClient := telemetry.New(registrationResponse.FunctionName, licenseKey, conf.TelemetryEndpoint, conf.LogEndpoint, batch, conf.CollectTraceID, conf.ClientTimeout) - telemetryChan, err := telemetry.InitTelemetryChannel() + // Step 2 - Start the local http listener which will receive data from Telemetry API + l.Debug("[main] Starting the Telemetry listener") + telemetryListener := telemetryApi.NewTelemetryApiListener() + telemetryListenerUri, err := telemetryListener.Start() if err != nil { - err2 := invocationClient.InitError(ctx, "telemetryClient.init", err) - if err2 != nil { - util.Logln(err2) - } - util.Panic("telemetry pipe init failed: ", err) + l.Fatal(err) } - // Run startup checks - go func() { - checks.RunChecks(ctx, conf, registrationResponse, telemetryClient) - }() - - // Send function logs as they arrive. When disabled, function logs aren't delivered to the extension. - backgroundTasks := &sync.WaitGroup{} - backgroundTasks.Add(1) - - go func() { - defer backgroundTasks.Done() - logShipLoop(ctx, logServer, telemetryClient) - }() - - // Call next, and process telemetry, until we're shut down - eventCounter := mainLoop(ctx, invocationClient, batch, telemetryChan, logServer, telemetryClient) - - util.Logf("New Relic Extension shutting down after %v events\n", eventCounter) - - err = logServer.Close() + // Step 3 - Subscribe the listener to Telemetry API + l.Debug("[main] Subscribing to the Telemetry API") + telemetryApiClient := telemetryApi.NewClient(conf.LogLevel) + _, err = telemetryApiClient.Subscribe(ctx, extensionId, telemetryListenerUri) if err != nil { - util.Logln("Error shutting down Log API server", err) + l.Fatal(err) } + l.Debug("[main] Subscription success") - pollLogServer(logServer, batch) - finalHarvest := batch.Close() - shipHarvest(ctx, finalHarvest, telemetryClient) - - util.Debugln("Waiting for background tasks to complete") - backgroundTasks.Wait() - - shutdownAt := time.Now() - ranFor := shutdownAt.Sub(extensionStartup) - util.Logf("Extension shutdown after %vms", ranFor.Milliseconds()) -} - -// logShipLoop ships function logs to New Relic as they arrive. -func logShipLoop(ctx context.Context, logServer *logserver.LogServer, telemetryClient *telemetry.Client) { - for { - functionLogs, more := logServer.AwaitFunctionLogs() - if !more { - return - } - - err := telemetryClient.SendFunctionLogs(ctx, invokedFunctionARN, functionLogs) - if err != nil { - util.Logf("Failed to send %d function logs", len(functionLogs)) - } - } -} + // Create Dispatch Manager + dispatcher := NewDispatcher(agentTelemetry.NewDispatcher(conf), telemetryApi.NewDispatcher( + &conf, + ctx, + conf.TelemetryAPIBatchSize, + )) -// mainLoop repeatedly calls the /next api, and processes telemetry and platform logs. The timing is rather complicated. -func mainLoop(ctx context.Context, invocationClient *client.InvocationClient, batch *telemetry.Batch, telemetryChan chan []byte, logServer *logserver.LogServer, telemetryClient *telemetry.Client) int { - eventCounter := 0 - probablyTimeout := false + l.Info("[main] New Relic Telemetry API Extension succesfully registered and subscribed") + // Will block until invoke or shutdown event is received or cancelled via the context. for { select { case <-ctx.Done(): - // We're already done - return eventCounter + return default: - // Our call to next blocks. It is likely that the container is frozen immediately after we call NextEvent. - event, err := invocationClient.NextEvent(ctx) - - // We've thawed. - eventStart := time.Now() + l.Debug("[main] Waiting for next event...") + // This is a blocking action + res, err := extensionApiClient.NextEvent(ctx) if err != nil { - util.Logln(err) - err = invocationClient.ExitError(ctx, "NextEventError.Main", err) - if err != nil { - util.Logln(err) - } - continue - } - - eventCounter++ - - if probablyTimeout { - // We suspect a timeout. Either way, we've gotten to the next event, so telemetry will - // have arrived for the last request if it's going to. Non-blocking poll for telemetry. - // If we have indeed timed out, there's a chance we got telemetry out anyway. If we haven't - // timed out, this will catch us up to the current state of telemetry, allowing us to resume. - select { - case telemetryBytes := <-telemetryChan: - // We received telemetry - util.Debugf("Agent telemetry bytes: %s", base64.URLEncoding.EncodeToString(telemetryBytes)) - batch.AddTelemetry(lastRequestId, telemetryBytes) - util.Logf("We suspected a timeout for request %s but got telemetry anyway", lastRequestId) - default: - } - } - - if event.EventType == api.Shutdown { - if event.ShutdownReason == api.Timeout && lastRequestId != "" { - // Synthesize the timeout error message that the platform produces, and LLC parses - timestamp := eventStart.UTC() - timeoutSecs := eventStart.Sub(lastEventStart).Seconds() - timeoutMessage := fmt.Sprintf( - "%s %s Task timed out after %.2f seconds", - timestamp.Format(time.RFC3339), - lastRequestId, - timeoutSecs, - ) - batch.AddTelemetry(lastRequestId, []byte(timeoutMessage)) - } else if event.ShutdownReason == api.Failure && lastRequestId != "" { - // Synthesize a generic platform error. Probably an OOM, though it could be any runtime crash. - errorMessage := fmt.Sprintf("RequestId: %s A platform error caused a shutdown", lastRequestId) - batch.AddTelemetry(lastRequestId, []byte(errorMessage)) - } - - return eventCounter - } else { - // Reset probablyTimeout if the event after the suspected timeout wasn't a timeout shutdown. - probablyTimeout = false + l.Errorf("[main] Exiting. Error: %v", err) + return } - - // Note: shutdown events do not have these properties; we now know this is an invocation event. - invokedFunctionARN = event.InvokedFunctionARN - lastRequestId = event.RequestID - - // Create an invocation record to hold telemetry - batch.AddInvocation(lastRequestId, eventStart) - - // Await agent telemetry, which may time out. - - // timeoutInstant is when the invocation will time out - timeoutInstant := time.Unix(0, event.DeadlineMs*int64(time.Millisecond)) - - // Set the timeout timer for a smidge before the actual timeout; we can recover from false timeouts. - timeoutWatchBegins := 200 * time.Millisecond - timeLimitContext, timeLimitCancel := context.WithDeadline(ctx, timeoutInstant.Add(-timeoutWatchBegins)) - - // Before we begin to await telemetry, harvest and ship. Ripe telemetry will mostly be handled here. Even that is a - // minority of invocations. Putting this here lets us run the HTTP request to send to NR in parallel with the Lambda - // handler, reducing or eliminating our latency impact. - pollLogServer(logServer, batch) - shipHarvest(ctx, batch.Harvest(time.Now()), telemetryClient) - - select { - case <-timeLimitContext.Done(): - timeLimitCancel() - - // We are about to timeout - probablyTimeout = true - continue - case telemetryBytes := <-telemetryChan: - timeLimitCancel() - - // We received telemetry - util.Debugf("Agent telemetry bytes: %s", base64.URLEncoding.EncodeToString(telemetryBytes)) - inv := batch.AddTelemetry(lastRequestId, telemetryBytes) - if inv == nil { - util.Logf("Failed to add telemetry for request %v", lastRequestId) - } - - // Opportunity for an aggressive harvest, in which case, we definitely want to wait for the HTTP POST - // to complete. Mostly, nothing really happens here. - pollLogServer(logServer, batch) - shipHarvest(ctx, batch.Harvest(time.Now()), telemetryClient) + l.Debugf("[main] Received event %+v", res) + + // Dispatching log events from previous invocations + dispatcher.Dispatch(ctx, telemetryListener.LogEventsQueue, res, false) + + if res.EventType == extensionApi.Invoke { + l.Debug("[handleInvoke]") + // we no longer care about this but keep it here just in case + } else if res.EventType == extensionApi.Shutdown { + // force dispatch all remaining telemetry, handle shutdown + l.Debug("[handleShutdown] a shutdown event has occured") + dispatcher.Dispatch(ctx, telemetryListener.LogEventsQueue, res, true) + l.Info("[main] New Relic Telemetry API Extension successfully shut down") + return } - - lastEventStart = eventStart } } } -// pollLogServer polls for platform logs, and annotates telemetry -func pollLogServer(logServer *logserver.LogServer, batch *telemetry.Batch) { - for _, platformLog := range logServer.PollPlatformChannel() { - inv := batch.AddTelemetry(platformLog.RequestID, platformLog.Content) - if inv == nil { - util.Debugf("Skipping platform log for request %v", platformLog.RequestID) - } - } +type Dispatcher struct { + agentDispatcher *agentTelemetry.AgentTelemetryDispatcher + TelemetryApiDispatcher *telemetryApi.Dispatcher } -func shipHarvest(ctx context.Context, harvested []*telemetry.Invocation, telemetryClient *telemetry.Client) { - if len(harvested) > 0 { - telemetrySlice := make([][]byte, 0, 2*len(harvested)) - for _, inv := range harvested { - telemetrySlice = append(telemetrySlice, inv.Telemetry...) - } - - err, _ := telemetryClient.SendTelemetry(ctx, invokedFunctionARN, telemetrySlice) - if err != nil { - util.Logf("Failed to send harvested telemetry for %d invocations %s", len(harvested), err) - } +func NewDispatcher(agentDispatcher *agentTelemetry.AgentTelemetryDispatcher, telemtryApiDispacther *telemetryApi.Dispatcher) *Dispatcher { + return &Dispatcher{ + agentDispatcher: agentDispatcher, + TelemetryApiDispatcher: telemtryApiDispacther, } } -func noopLoop(ctx context.Context, invocationClient *client.InvocationClient) { - util.Logln("Starting no-op mode, no telemetry will be sent") +func (d *Dispatcher) Dispatch(ctx context.Context, logQueue *queue.Queue, eventResponse *extensionApi.NextEventResponse, force bool) { + startDispatch := time.Now() + wg := sync.WaitGroup{} + wg.Add(2) - for { - select { - case <-ctx.Done(): - return - default: - event, err := invocationClient.NextEvent(ctx) - if err != nil { - util.Logln(err) - errErr := invocationClient.ExitError(ctx, "NextEventError.Noop", err) - if errErr != nil { - util.Logln(errErr) - } - continue - } + go func() { + defer wg.Done() + d.agentDispatcher.Dispatch(ctx, eventResponse, force) + }() + go func() { + defer wg.Done() + d.TelemetryApiDispatcher.Dispatch(ctx, logQueue, eventResponse, force) + }() - if event.EventType == api.Shutdown { - return - } - } - } + l.Debug("[main: Dispatch] waiting for all telemetry to dispatch...") + wg.Wait() + l.Debugf("[main: Dispatch] dispatching all telemetry took %s", time.Since(startDispatch).String()) } diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 3f625ad5..00000000 --- a/main_test.go +++ /dev/null @@ -1,753 +0,0 @@ -//go:build !race -// +build !race - -package main - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "os" - "testing" - "time" - - "github.com/newrelic/newrelic-lambda-extension/lambda/extension/api" - "github.com/newrelic/newrelic-lambda-extension/util" - - "github.com/stretchr/testify/assert" -) - -// TODO: These tests are very repetitive. Helpers would be useful here. - -func TestMainRegisterFail(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - if r.URL.Path == "/2020-01-01/extension/register" { - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(400) - _, _ = w.Write(nil) - } - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - assert.Panics(t, main) -} - -func TestMainLogServerInitFail(t *testing.T) { - var ( - registerRequestCount int - initErrorRequestCount int - exitErrorRequestCount int - logRegisterRequestCount int - ) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - if r.URL.Path == "/2020-01-01/extension/register" { - registerRequestCount++ - - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(200) - res, err := json.Marshal(api.RegistrationResponse{ - FunctionName: "foobar", - FunctionVersion: "latest", - Handler: "lambda.handler", - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - - if r.URL.Path == "/2020-01-01/extension/init/error" { - initErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/exit/error" { - exitErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-08-15/logs" { - logRegisterRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - _ = os.Setenv("NEW_RELIC_LICENSE_KEY", "foobar") - defer os.Unsetenv("NEW_RELIC_LICENSE_KEY") - - // Shouldn't be able to bind to this locally - _ = os.Setenv("NEW_RELIC_LOG_SERVER_HOST", "sandbox.localdomain") - defer os.Unsetenv("NEW_RELIC_LOG_SERVER_HOST") - - _ = os.Setenv("NEW_RELIC_EXTENSION_LOG_LEVEL", "DEBUG") - defer os.Unsetenv("NEW_RELIC_EXTENSION_LOG_LEVEL") - - assert.Panics(t, main) - - assert.Equal(t, 1, registerRequestCount) - assert.Equal(t, 1, initErrorRequestCount) - assert.Equal(t, 0, exitErrorRequestCount) - assert.Equal(t, 0, logRegisterRequestCount) -} - -func TestMainLogServerRegisterFail(t *testing.T) { - var ( - registerRequestCount int - initErrorRequestCount int - exitErrorRequestCount int - logRegisterRequestCount int - ) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - if r.URL.Path == "/2020-01-01/extension/register" { - registerRequestCount++ - - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(200) - res, err := json.Marshal(api.RegistrationResponse{ - FunctionName: "foobar", - FunctionVersion: "latest", - Handler: "lambda.handler", - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - - if r.URL.Path == "/2020-01-01/extension/init/error" { - initErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/exit/error" { - exitErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-08-15/logs" { - logRegisterRequestCount++ - - w.WriteHeader(400) - _, _ = w.Write(nil) - } - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - _ = os.Setenv("NEW_RELIC_LICENSE_KEY", "foobar") - defer os.Unsetenv("NEW_RELIC_LICENSE_KEY") - - _ = os.Setenv("NEW_RELIC_LOG_SERVER_HOST", "localhost") - defer os.Unsetenv("NEW_RELIC_LOG_SERVER_HOST") - - _ = os.Setenv("NEW_RELIC_EXTENSION_LOG_LEVEL", "DEBUG") - defer os.Unsetenv("NEW_RELIC_EXTENSION_LOG_LEVEL") - - assert.Panics(t, main) - - assert.Equal(t, 1, registerRequestCount) - assert.Equal(t, 1, initErrorRequestCount) - assert.Equal(t, 0, exitErrorRequestCount) - assert.Equal(t, 1, logRegisterRequestCount) -} - -func TestMainShutdown(t *testing.T) { - var ( - registerRequestCount int - initErrorRequestCount int - exitErrorRequestCount int - logRegisterRequestCount int - nextEventRequestCount int - ) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - util.Logln("Path: ", r.URL.Path) - defer util.Close(r.Body) - - if r.URL.Path == "/2020-01-01/extension/register" { - registerRequestCount++ - - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(200) - res, err := json.Marshal(api.RegistrationResponse{ - FunctionName: "foobar", - FunctionVersion: "latest", - Handler: "lambda.handler", - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - - if r.URL.Path == "/2020-01-01/extension/init/error" { - initErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/exit/error" { - exitErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-08-15/logs" { - logRegisterRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/event/next" { - nextEventRequestCount++ - - w.WriteHeader(200) - res, err := json.Marshal(api.InvocationEvent{ - EventType: api.Shutdown, - DeadlineMs: 1, - RequestID: "12345", - InvokedFunctionARN: "arn:aws:lambda:us-east-1:12345:foobar", - ShutdownReason: api.Timeout, - Tracing: nil, - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - _ = os.Setenv("NEW_RELIC_LICENSE_KEY", "foobar") - defer os.Unsetenv("NEW_RELIC_LICENSE_KEY") - - _ = os.Setenv("NEW_RELIC_LOG_SERVER_HOST", "localhost") - defer os.Unsetenv("NEW_RELIC_LOG_SERVER_HOST") - - _ = os.Setenv("NEW_RELIC_EXTENSION_LOG_LEVEL", "DEBUG") - defer os.Unsetenv("NEW_RELIC_EXTENSION_LOG_LEVEL") - - assert.NotPanics(t, main) - - assert.Equal(t, 1, registerRequestCount) - assert.Equal(t, 0, initErrorRequestCount) - assert.Equal(t, 0, exitErrorRequestCount) - assert.Equal(t, 1, logRegisterRequestCount) - assert.Equal(t, 1, nextEventRequestCount) -} - -func TestMainNoLicenseKey(t *testing.T) { - var ( - registerRequestCount int - initErrorRequestCount int - exitErrorRequestCount int - logRegisterRequestCount int - nextEventRequestCount int - ) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - util.Logln("Path: ", r.URL.Path) - defer util.Close(r.Body) - - if r.URL.Path == "/2020-01-01/extension/register" { - registerRequestCount++ - - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(200) - res, err := json.Marshal(api.RegistrationResponse{ - FunctionName: "foobar", - FunctionVersion: "latest", - Handler: "lambda.handler", - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - - if r.URL.Path == "/2020-01-01/extension/init/error" { - initErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/exit/error" { - exitErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-08-15/logs" { - logRegisterRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/event/next" { - nextEventRequestCount++ - - w.WriteHeader(200) - res, err := json.Marshal(api.InvocationEvent{ - EventType: api.Shutdown, - DeadlineMs: 1, - RequestID: "12345", - InvokedFunctionARN: "arn:aws:lambda:us-east-1:12345:foobar", - ShutdownReason: api.Timeout, - Tracing: nil, - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - _ = os.Setenv("NEW_RELIC_EXTENSION_LOG_LEVEL", "DEBUG") - defer os.Unsetenv("NEW_RELIC_EXTENSION_LOG_LEVEL") - - assert.NotPanics(t, main) - - assert.Equal(t, 1, registerRequestCount) - assert.Equal(t, 0, initErrorRequestCount) - assert.Equal(t, 0, exitErrorRequestCount) - assert.Equal(t, 0, logRegisterRequestCount) - assert.Equal(t, 1, nextEventRequestCount) -} - -func TestMainExtensionDisabled(t *testing.T) { - var ( - registerRequestCount int - initErrorRequestCount int - exitErrorRequestCount int - logRegisterRequestCount int - nextEventRequestCount int - ) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - util.Logln("Path: ", r.URL.Path) - defer util.Close(r.Body) - - if r.URL.Path == "/2020-01-01/extension/register" { - registerRequestCount++ - - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(200) - res, err := json.Marshal(api.RegistrationResponse{ - FunctionName: "foobar", - FunctionVersion: "latest", - Handler: "lambda.handler", - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - - if r.URL.Path == "/2020-01-01/extension/init/error" { - initErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/exit/error" { - exitErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-08-15/logs" { - logRegisterRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/event/next" { - nextEventRequestCount++ - - w.WriteHeader(200) - res, err := json.Marshal(api.InvocationEvent{ - EventType: api.Shutdown, - DeadlineMs: 1, - RequestID: "12345", - InvokedFunctionARN: "arn:aws:lambda:us-east-1:12345:foobar", - ShutdownReason: api.Timeout, - Tracing: nil, - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - _ = os.Setenv("NEW_RELIC_LICENSE_KEY", "foobar") - defer os.Unsetenv("NEW_RELIC_LICENSE_KEY") - - _ = os.Setenv("NEW_RELIC_LAMBDA_EXTENSION_ENABLED", "false") - defer os.Unsetenv("NEW_RELIC_LAMBDA_EXTENSION_ENABLED") - - _ = os.Setenv("NEW_RELIC_EXTENSION_LOG_LEVEL", "DEBUG") - defer os.Unsetenv("NEW_RELIC_EXTENSION_LOG_LEVEL") - - assert.NotPanics(t, main) - - assert.Equal(t, 1, registerRequestCount) - assert.Equal(t, 0, initErrorRequestCount) - assert.Equal(t, 0, exitErrorRequestCount) - assert.Equal(t, 0, logRegisterRequestCount) - assert.Equal(t, 1, nextEventRequestCount) -} - -func TestMainTimeout(t *testing.T) { - var ( - registerRequestCount int - initErrorRequestCount int - exitErrorRequestCount int - logRegisterRequestCount int - nextEventRequestCount int - ) - - ctx, cancel := context.WithCancel(context.Background()) - overrideContext(ctx) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - if r.URL.Path == "/2020-01-01/extension/register" { - registerRequestCount++ - - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(200) - res, err := json.Marshal(api.RegistrationResponse{ - FunctionName: "foobar", - FunctionVersion: "latest", - Handler: "lambda.handler", - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - - if r.URL.Path == "/2020-01-01/extension/init/error" { - initErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/exit/error" { - exitErrorRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-08-15/logs" { - logRegisterRequestCount++ - - w.WriteHeader(200) - _, _ = w.Write(nil) - - } - - if r.URL.Path == "/2020-01-01/extension/event/next" { - nextEventRequestCount++ - - w.WriteHeader(200) - res, err := json.Marshal(api.InvocationEvent{ - EventType: api.Invoke, - DeadlineMs: 1000, - RequestID: "12345", - InvokedFunctionARN: "arn:aws:lambda:us-east-1:12345:foobar", - ShutdownReason: "", - Tracing: nil, - }) - assert.Nil(t, err) - _, _ = w.Write(res) - - cancel() - } - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - _ = os.Setenv("NEW_RELIC_LICENSE_KEY", "foobar") - defer os.Unsetenv("NEW_RELIC_LICENSE_KEY") - - _ = os.Setenv("NEW_RELIC_LOG_SERVER_HOST", "localhost") - defer os.Unsetenv("NEW_RELIC_LOG_SERVER_HOST") - - _ = os.Setenv("NEW_RELIC_EXTENSION_LOG_LEVEL", "DEBUG") - defer os.Unsetenv("NEW_RELIC_EXTENSION_LOG_LEVEL") - - assert.NotPanics(t, main) - - assert.Equal(t, 1, registerRequestCount) - assert.Equal(t, 0, initErrorRequestCount) - assert.Equal(t, 0, exitErrorRequestCount) - assert.Equal(t, 1, logRegisterRequestCount) - assert.Equal(t, 1, nextEventRequestCount) -} - -func TestMainTimeoutUnreachable(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(200*time.Millisecond)) - defer cancel() - overrideContext(ctx) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - if r.URL.Path == "/2020-01-01/extension/register" { - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(200) - res, err := json.Marshal(api.RegistrationResponse{ - FunctionName: "foobar", - FunctionVersion: "$latest", - Handler: "lambda.handler", - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - - if r.URL.Path == "/2020-01-01/extension/init/error" { - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/exit/error" { - w.WriteHeader(200) - _, _ = w.Write(nil) - } - - if r.URL.Path == "/2020-08-15/logs" { - w.WriteHeader(200) - _, _ = w.Write(nil) - } - - if r.URL.Path == "/2020-01-01/extension/event/next" { - time.Sleep(25 * time.Millisecond) - - w.WriteHeader(200) - res, err := json.Marshal(api.InvocationEvent{ - EventType: api.Invoke, - DeadlineMs: 100, - RequestID: "12345", - InvokedFunctionARN: "arn:aws:lambda:us-east-1:12345:foobar", - ShutdownReason: "", - Tracing: nil, - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - - if r.URL.Path == "/aws/lambda/v1" { - time.Sleep(5 * time.Second) - - w.WriteHeader(200) - _, _ = w.Write(nil) - } - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - _ = os.Setenv("NEW_RELIC_LICENSE_KEY", "foobar") - defer os.Unsetenv("NEW_RELIC_LICENSE_KEY") - - _ = os.Setenv("NEW_RELIC_LOG_SERVER_HOST", "localhost") - defer os.Unsetenv("NEW_RELIC_LOG_SERVER_HOST") - - _ = os.Setenv("NEW_RELIC_EXTENSION_LOG_LEVEL", "DEBUG") - defer os.Unsetenv("NEW_RELIC_EXTENSION_LOG_LEVEL") - - _ = os.Setenv("NEW_RELIC_TELEMETRY_ENDPOINT", fmt.Sprintf("%s/aws/lambda/v1", srv.URL)) - defer os.Unsetenv("NEW_RELIC_TELEMETRY_ENDPOINT") - - _ = os.Remove("/tmp/newrelic-telemetry") - - go func() { - pipeOpened := false - - for { - select { - case <-ctx.Done(): - return - default: - if _, err := os.Stat("/tmp/newrelic-telemetry"); os.IsNotExist(err) { - if pipeOpened { - return - } else { - continue - } - } else { - pipeOpened = true - } - - pipe, err := os.OpenFile("/tmp/newrelic-telemetry", os.O_WRONLY, 0) - assert.Nil(t, err) - defer pipe.Close() - - pipe.WriteString("foobar\n") - pipe.Close() - time.Sleep(100 * time.Millisecond) - } - } - }() - - assert.NotPanics(t, main) -} - -func TestMainTimeoutNoPipeWrite(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(200*time.Millisecond)) - defer cancel() - overrideContext(ctx) - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer util.Close(r.Body) - - if r.URL.Path == "/2020-01-01/extension/register" { - w.Header().Add(api.ExtensionIdHeader, "test-ext-id") - w.WriteHeader(200) - res, err := json.Marshal(api.RegistrationResponse{ - FunctionName: "foobar", - FunctionVersion: "$latest", - Handler: "lambda.handler", - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - - if r.URL.Path == "/2020-01-01/extension/init/error" { - w.WriteHeader(200) - _, _ = w.Write([]byte("")) - } - - if r.URL.Path == "/2020-01-01/extension/exit/error" { - w.WriteHeader(200) - _, _ = w.Write(nil) - } - - if r.URL.Path == "/2020-08-15/logs" { - w.WriteHeader(200) - _, _ = w.Write(nil) - } - - if r.URL.Path == "/2020-01-01/extension/event/next" { - time.Sleep(25 * time.Millisecond) - - w.WriteHeader(200) - res, err := json.Marshal(api.InvocationEvent{ - EventType: api.Invoke, - DeadlineMs: 100, - RequestID: "12345", - InvokedFunctionARN: "arn:aws:lambda:us-east-1:12345:foobar", - ShutdownReason: "", - Tracing: nil, - }) - assert.Nil(t, err) - _, _ = w.Write(res) - } - })) - defer srv.Close() - - url := srv.URL[7:] - - _ = os.Setenv(api.LambdaHostPortEnvVar, url) - defer os.Unsetenv(api.LambdaHostPortEnvVar) - - _ = os.Setenv("NEW_RELIC_LICENSE_KEY", "foobar") - defer os.Unsetenv("NEW_RELIC_LICENSE_KEY") - - _ = os.Setenv("NEW_RELIC_LOG_SERVER_HOST", "localhost") - defer os.Unsetenv("NEW_RELIC_LOG_SERVER_HOST") - - _ = os.Setenv("NEW_RELIC_EXTENSION_LOG_LEVEL", "DEBUG") - defer os.Unsetenv("NEW_RELIC_EXTENSION_LOG_LEVEL") - - _ = os.Remove("/tmp/newrelic-telemetry") - - go func() { - pipeOpened := false - - for { - select { - case <-ctx.Done(): - return - default: - if _, err := os.Stat("/tmp/newrelic-telemetry"); os.IsNotExist(err) { - if pipeOpened { - return - } else { - continue - } - } else { - pipeOpened = true - } - - pipe, err := os.OpenFile("/tmp/newrelic-telemetry", os.O_WRONLY, 0) - assert.Nil(t, err) - defer pipe.Close() - - time.Sleep(200 * time.Millisecond) - pipe.Close() - } - } - }() - - assert.NotPanics(t, main) -} - -func overrideContext(ctx context.Context) { - rootCtx = ctx -} diff --git a/telemetry/batch.go b/telemetry/batch.go deleted file mode 100644 index 7b9a573c..00000000 --- a/telemetry/batch.go +++ /dev/null @@ -1,178 +0,0 @@ -package telemetry - -import ( - "math" - "sync" - "time" - - "github.com/newrelic/newrelic-lambda-extension/util" -) - -// The Unix epoch instant; used as a nil time for eldest and lastHarvest -var epochStart = time.Unix(0, 0) - -// Batch represents the unsent invocations and their telemetry, along with timing data. -type Batch struct { - extractTraceID bool - lastHarvest time.Time - eldest time.Time - ripeDuration time.Duration - veryOldDuration time.Duration - invocations map[string]*Invocation - lock sync.RWMutex -} - -// NewBatch constructs a new batch. -func NewBatch(ripeMillis, rotMillis int64, extractTraceID bool) *Batch { - initialSize := uint32(math.Min(float64(ripeMillis)/100, 100)) - return &Batch{ - lastHarvest: epochStart, - eldest: epochStart, - invocations: make(map[string]*Invocation, initialSize), - ripeDuration: time.Duration(ripeMillis) * time.Millisecond, - veryOldDuration: time.Duration(rotMillis) * time.Millisecond, - extractTraceID: extractTraceID, - } -} - -// AddInvocation should be called just after the next API response. It creates the Invocation record so that we can attach telemetry later. -func (b *Batch) AddInvocation(requestId string, start time.Time) { - b.lock.Lock() - defer b.lock.Unlock() - - invocation := NewInvocation(requestId, start) - b.invocations[requestId] = &invocation -} - -// AddTelemetry attaches telemetry to an existing Invocation, identified by requestId -func (b *Batch) AddTelemetry(requestId string, telemetry []byte) *Invocation { - b.lock.Lock() - defer b.lock.Unlock() - - inv, ok := b.invocations[requestId] - if ok { - inv.Telemetry = append(inv.Telemetry, telemetry) - if b.eldest.Equal(epochStart) { - b.eldest = inv.Start - } - if b.extractTraceID { - traceId, err := ExtractTraceID(telemetry) - if err != nil { - util.Debugln(err) - } - // We don't want to unset a previously set trace ID - if traceId != "" { - inv.TraceId = traceId - } - } - return inv - } - return nil -} - -// Harvest checks to see if it's time to harvest, and returns harvested invocations, or nil. The caller must ensure that harvested invocations are sent. -func (b *Batch) Harvest(now time.Time) []*Invocation { - b.lock.Lock() - defer b.lock.Unlock() - - if len(b.invocations) == 0 { - return nil - } - - veryOldTime := now.Add(-b.veryOldDuration) - if b.lastHarvest.Before(veryOldTime) { - return b.aggressiveHarvest(now) - } - - ripeTime := now.Add(-b.ripeDuration) - if b.eldest.Before(ripeTime) { - return b.ripeHarvest(now) - } - - return nil -} - -// Close aggressively harvests all telemetry from the Batch. The Batch is no longer valid. -func (b *Batch) Close() []*Invocation { - b.lock.Lock() - defer b.lock.Unlock() - - return b.aggressiveHarvest(time.Now()) -} - -// aggressiveHarvest harvests all invocations, ripe or not. It removes harvested invocations from the batch and updates the lastHarvest timestamp. -func (b *Batch) aggressiveHarvest(now time.Time) []*Invocation { - ret := make([]*Invocation, 0, len(b.invocations)) - for k, v := range b.invocations { - if !v.IsEmpty() { - ret = append(ret, v) - delete(b.invocations, k) - } - } - if len(ret) > 0 { - b.lastHarvest = now - b.eldest = epochStart - } - util.Debugf("Aggressive harvest yielded %d invocations\n", len(ret)) - return ret -} - -// ripeHarvest harvests all ripe invocations. It removes harvested invocations from the batch and updates the lastHarvest and eldest timestamps. -func (b *Batch) ripeHarvest(now time.Time) []*Invocation { - ret := make([]*Invocation, 0, len(b.invocations)) - newEldest := epochStart - for k, v := range b.invocations { - if v.IsRipe() { - ret = append(ret, v) - delete(b.invocations, k) - } else if newEldest.Equal(epochStart) || v.Start.Before(newEldest) { - newEldest = v.Start - } - } - b.eldest = newEldest - if len(ret) > 0 { - b.lastHarvest = now - } - util.Debugf("Ripe harvest yielded %d invocations\n", len(ret)) - return ret -} - -// RetrieveTraceID looks up a trace ID using the provided request ID -func (b *Batch) RetrieveTraceID(requestId string) string { - b.lock.RLock() - defer b.lock.RUnlock() - - inv, ok := b.invocations[requestId] - if ok { - return inv.TraceId - } - return "" -} - -// An Invocation holds telemetry for a request, and knows when the request began. -// Invocations are parts of a Batch, and should only be used by the batch object. -type Invocation struct { - Start time.Time - RequestId string - TraceId string - Telemetry [][]byte -} - -// NewInvocation creates an Invocation, which can hold telemetry -func NewInvocation(requestId string, start time.Time) Invocation { - return Invocation{ - Start: start, - RequestId: requestId, - Telemetry: make([][]byte, 0, 2), - } -} - -// IsRipe indicates that an Invocation has all the telemetry it's likely to get. Sending a ripe invocation won't omit data. -func (inv *Invocation) IsRipe() bool { - return len(inv.Telemetry) >= 2 -} - -// IsEmpty is true when the invocation has no telemetry. The invocation has begun, but has received no agent payload, nor platform logs. -func (inv *Invocation) IsEmpty() bool { - return len(inv.Telemetry) == 0 -} diff --git a/telemetry/batch_test.go b/telemetry/batch_test.go deleted file mode 100644 index f8f56a09..00000000 --- a/telemetry/batch_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package telemetry - -import ( - "bytes" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -const ( - testTelemetry = "test_telemetry" - moreTestTelemetry = "more_test_telemetry" - testRequestId = "test_a" - testRequestId2 = "test_b" - testRequestId3 = "test_c" - testNoSuchRequestId = "test_z" - ripe = 1000 - rot = 10000 -) - -var ( - requestStart = time.Unix(1603821157, 0) -) - -func TestMissingInvocation(t *testing.T) { - batch := NewBatch(ripe, rot, false) - - invocation := batch.AddTelemetry(testNoSuchRequestId, bytes.NewBufferString(testTelemetry).Bytes()) - assert.Nil(t, invocation) -} - -func TestEmptyHarvest(t *testing.T) { - batch := NewBatch(ripe, rot, false) - res := batch.Harvest(requestStart) - - assert.Nil(t, res) -} - -func TestEmptyRotHarvest(t *testing.T) { - batch := NewBatch(ripe, rot, false) - - batch.AddInvocation("test", requestStart) - - res := batch.Harvest(requestStart) - - assert.Empty(t, res) -} - -func TestEmptyRipeHarvest(t *testing.T) { - batch := NewBatch(ripe, rot, false) - - batch.lastHarvest = requestStart.Add(-ripe) - batch.AddInvocation("test", requestStart) - - res := batch.Harvest(requestStart) - - assert.Empty(t, res) -} - -func TestWithInvocationRipeHarvest(t *testing.T) { - batch := NewBatch(ripe, rot, false) - - batch.lastHarvest = requestStart - - batch.AddInvocation(testRequestId, requestStart) - batch.AddInvocation(testRequestId2, requestStart.Add(100*time.Millisecond)) - batch.AddInvocation(testRequestId3, requestStart.Add(200*time.Millisecond)) - - invocation := batch.AddTelemetry(testRequestId, bytes.NewBufferString(testTelemetry).Bytes()) - assert.NotNil(t, invocation) - - invocation2 := batch.AddTelemetry(testRequestId, bytes.NewBufferString(moreTestTelemetry).Bytes()) - assert.Equal(t, invocation, invocation2) - - batch.AddTelemetry(testRequestId2, bytes.NewBufferString(testTelemetry).Bytes()) - - harvested := batch.Harvest(requestStart.Add(ripe*time.Millisecond + time.Millisecond)) - assert.Equal(t, 1, len(harvested)) - assert.Equal(t, testRequestId, harvested[0].RequestId) - assert.Equal(t, 2, len(harvested[0].Telemetry)) -} - -func TestWithInvocationAggressiveHarvest(t *testing.T) { - batch := NewBatch(ripe, rot, false) - - batch.AddInvocation(testRequestId, requestStart) - batch.AddInvocation(testRequestId2, requestStart.Add(100*time.Millisecond)) - batch.AddInvocation(testRequestId3, requestStart.Add(200*time.Millisecond)) - - invocation := batch.AddTelemetry(testRequestId, bytes.NewBufferString(testTelemetry).Bytes()) - assert.NotNil(t, invocation) - - invocation2 := batch.AddTelemetry(testRequestId, bytes.NewBufferString(moreTestTelemetry).Bytes()) - assert.Equal(t, invocation, invocation2) - - batch.AddTelemetry(testRequestId2, bytes.NewBufferString(testTelemetry).Bytes()) - - harvested := batch.Harvest(requestStart.Add(ripe*time.Millisecond + time.Millisecond)) - assert.Equal(t, 2, len(harvested)) -} - -func TestBatch_Close(t *testing.T) { - batch := NewBatch(ripe, rot, false) - - batch.AddInvocation(testRequestId, requestStart) - batch.AddInvocation(testRequestId2, requestStart.Add(100*time.Millisecond)) - batch.AddInvocation(testRequestId3, requestStart.Add(200*time.Millisecond)) - - invocation := batch.AddTelemetry(testRequestId, bytes.NewBufferString(testTelemetry).Bytes()) - assert.NotNil(t, invocation) - - invocation2 := batch.AddTelemetry(testRequestId, bytes.NewBufferString(moreTestTelemetry).Bytes()) - assert.Equal(t, invocation, invocation2) - - batch.AddTelemetry(testRequestId2, bytes.NewBufferString(testTelemetry).Bytes()) - - harvested := batch.Close() - assert.Equal(t, 2, len(harvested)) -} - -func TestBatchAsync(t *testing.T) { - batch := NewBatch(ripe, rot, false) - - batch.lastHarvest = requestStart - - wg := sync.WaitGroup{} - wg.Add(3) - - go func() { - batch.AddInvocation(testRequestId, requestStart) - wg.Done() - }() - go func() { - batch.AddInvocation(testRequestId2, requestStart.Add(100*time.Millisecond)) - wg.Done() - }() - go func() { - batch.AddInvocation(testRequestId3, requestStart.Add(200*time.Millisecond)) - wg.Done() - }() - - // Doing this to try to trigger a panic - go batch.RetrieveTraceID(testRequestId) - - wg.Wait() - - var invocation, invocation2 *Invocation - wg.Add(2) - - go func() { - invocation = batch.AddTelemetry(testRequestId, bytes.NewBufferString(testTelemetry).Bytes()) - wg.Done() - }() - go func() { - invocation2 = batch.AddTelemetry(testRequestId, bytes.NewBufferString(moreTestTelemetry).Bytes()) - wg.Done() - }() - - // Doing this to try to trigger a panic - go batch.RetrieveTraceID(testRequestId) - - wg.Wait() - assert.NotNil(t, invocation) - assert.Equal(t, invocation, invocation2) - - batch.AddTelemetry(testRequestId2, bytes.NewBufferString(testTelemetry).Bytes()) - - harvested := batch.Harvest(requestStart.Add(ripe*time.Millisecond + time.Millisecond)) - go assert.Equal(t, 1, len(harvested)) - go assert.Equal(t, testRequestId, harvested[0].RequestId) - go assert.Equal(t, 2, len(harvested[0].Telemetry)) -} diff --git a/telemetryApi/client.go b/telemetryApi/client.go new file mode 100644 index 00000000..173e5682 --- /dev/null +++ b/telemetryApi/client.go @@ -0,0 +1,197 @@ +package telemetryApi + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + + "net/http" + "os" + + "github.com/pkg/errors" + + log "github.com/sirupsen/logrus" +) + +const lambdaAgentIdentifierHeaderKey string = "Lambda-Extension-Identifier" + +var l = log.WithFields(log.Fields{"pkg": "telemetryApi"}) + +// The client used for subscribing to the Telemetry API +type Client struct { + httpClient *http.Client + baseUrl string +} + +func NewClient(logLevel log.Level) *Client { + log.SetLevel(logLevel) + baseUrl := fmt.Sprintf("http://%s/2022-07-01/telemetry", os.Getenv("AWS_LAMBDA_RUNTIME_API")) + return &Client{ + httpClient: &http.Client{}, + baseUrl: baseUrl, + } +} + +// Represents the type of log events in Lambda +type EventType string + +const ( + // Used to receive log events emitted by the platform + Platform EventType = "platform" + // Used to receive log events emitted by the function + Function EventType = "function" + // Used is to receive log events emitted by the extension + Extension EventType = "extension" +) + +// Configuration for receiving telemetry from the Telemetry API. +// Telemetry will be sent to your listener when one of the conditions below is met. +type BufferingCfg struct { + // Maximum number of log events to be buffered in memory. (default: 10000, minimum: 1000, maximum: 10000) + MaxItems uint32 `json:"maxItems"` + // Maximum size in bytes of the log events to be buffered in memory. (default: 262144, minimum: 262144, maximum: 1048576) + MaxBytes uint32 `json:"maxBytes"` + // Maximum time (in milliseconds) for a batch to be buffered. (default: 1000, minimum: 100, maximum: 30000) + TimeoutMS uint32 `json:"timeoutMs"` +} + +// URI is used to set the endpoint where the logs will be sent to +type URI string + +// HttpMethod represents the HTTP method used to receive logs from Logs API +type HttpMethod string + +const ( + // Receive log events via POST requests to the listener + HttpPost HttpMethod = "POST" + // Receive log events via PUT requests to the listener + HttpPut HttpMethod = "PUT" +) + +// Used to specify the protocol when subscribing to Telemetry API for HTTP +type HttpProtocol string + +const ( + HttpProto HttpProtocol = "HTTP" +) + +// Denotes what the content is encoded in +type HttpEncoding string + +const ( + JSON HttpEncoding = "JSON" +) + +// Configuration for listeners that would like to receive telemetry via HTTP +type Destination struct { + Protocol HttpProtocol `json:"protocol"` + URI URI `json:"URI"` + HttpMethod HttpMethod `json:"method"` + Encoding HttpEncoding `json:"encoding"` +} + +type SchemaVersion string + +const ( + SchemaVersion20220701 = "2022-07-01" + SchemaVersionLatest = SchemaVersion20220701 +) + +// Request body that is sent to the Telemetry API on subscribe +type SubscribeRequest struct { + SchemaVersion SchemaVersion `json:"schemaVersion"` + EventTypes []EventType `json:"types"` + BufferingCfg BufferingCfg `json:"buffering"` + Destination Destination `json:"destination"` +} + +// Response body that is received from the Telemetry API on subscribe +type SubscribeResponse struct { + body string +} + +// Subscribes to the Telemetry API to start receiving the log events +func (c *Client) Subscribe(ctx context.Context, extensionId string, listenerUri string) (*SubscribeResponse, error) { + eventTypes := []EventType{ + Platform, + Function, + Extension, + } + + bufferingConfig := BufferingCfg{ + MaxItems: 1000, + MaxBytes: 256 * 1024, + TimeoutMS: 1000, + } + + destination := Destination{ + Protocol: HttpProto, + HttpMethod: HttpPost, + Encoding: JSON, + URI: URI(listenerUri), + } + + data, err := json.Marshal( + &SubscribeRequest{ + SchemaVersion: SchemaVersionLatest, + EventTypes: eventTypes, + BufferingCfg: bufferingConfig, + Destination: destination, + }) + + if err != nil { + return nil, errors.WithMessage(err, "Failed to marshal SubscribeRequest") + } + + headers := make(map[string]string) + headers[lambdaAgentIdentifierHeaderKey] = extensionId + + l.Debug("[client:Subscribe] Subscribing using baseUrl:", c.baseUrl) + resp, err := httpPutWithHeaders(ctx, c.httpClient, c.baseUrl, data, &headers) + if err != nil { + l.Error("[client:Subscribe] Subscription failed:", err) + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusAccepted { + l.Error("[client:Subscribe] Subscription failed. Logs API is not supported! Is this extension running in a local sandbox?") + } else if resp.StatusCode != http.StatusOK { + l.Error("[client:Subscribe] Subscription failed") + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Errorf("%s failed: %d[%s]", c.baseUrl, resp.StatusCode, resp.Status) + } + + return nil, errors.Errorf("%s failed: %d[%s] %s", c.baseUrl, resp.StatusCode, resp.Status, string(body)) + } + + body, _ := io.ReadAll(resp.Body) + l.Debug("[client:Subscribe] Subscription success:", string(body)) + + return &SubscribeResponse{string(body)}, nil +} + +func httpPutWithHeaders(ctx context.Context, client *http.Client, url string, data []byte, headers *map[string]string) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, "PUT", url, bytes.NewBuffer(data)) + if err != nil { + return nil, err + } + + contentType := "application/json" + req.Header.Set("Content-Type", contentType) + if headers != nil { + for k, v := range *headers { + req.Header.Set(k, v) + } + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/telemetryApi/dispatcher.go b/telemetryApi/dispatcher.go new file mode 100644 index 00000000..8f19994b --- /dev/null +++ b/telemetryApi/dispatcher.go @@ -0,0 +1,77 @@ +package telemetryApi + +import ( + "context" + "net/http" + "os" + + "github.com/golang-collections/go-datastructures/queue" + + "newrelic-lambda-extension/config" + "newrelic-lambda-extension/extensionApi" + "newrelic-lambda-extension/util" +) + +type Dispatcher struct { + httpClient *http.Client + compressTool *util.CompressTool + licenseKey string + accountID string + arn string + functionName string + minBatchSize int64 +} + +func GetNewRelicLicenseKey(ctx context.Context) string { + licenseKey := os.Getenv("NEW_RELIC_LICENSE_KEY") + + var err error + if len(licenseKey) == 0 { + licenseKey, err = getNewRelicLicenseKey(ctx) + if err != nil { + l.Fatalf("failed to get New Relic license key: %v", err) + } + } + if len(licenseKey) == 0 { + l.Fatal("NEW_RELIC_LICENSE_KEY undefined or unavailable") + } + + return licenseKey +} + +func NewDispatcher(config *config.Config, ctx context.Context, batchSize int64) *Dispatcher { + disp := &Dispatcher{ + httpClient: &http.Client{}, + licenseKey: config.LicenseKey, + minBatchSize: batchSize, + accountID: config.AccountID, + functionName: config.ExtensionName, + compressTool: util.NewCompressTool(), + } + + l.Tracef("Dispatcher: %+v", disp) + return disp +} + +func (d *Dispatcher) Dispatch(ctx context.Context, logEventsQueue *queue.Queue, lambdaEvent *extensionApi.NextEventResponse, force bool) { + if !logEventsQueue.Empty() && (force || logEventsQueue.Len() >= d.minBatchSize) { + l.Debug("[dispatcher:Dispatch] Dispatching ", logEventsQueue.Len(), " log events") + logEntries, _ := logEventsQueue.Get(logEventsQueue.Len()) + + if lambdaEvent.InvokedFunctionArn != "" && lambdaEvent.InvokedFunctionArn != d.arn { + if len(lambdaEvent.InvokedFunctionArn) > MaxAttributeValueLen { + d.arn = lambdaEvent.InvokedFunctionArn[:MaxAttributeValueLen] + } else { + d.arn = lambdaEvent.InvokedFunctionArn + } + } + + err := sendDataToNR(ctx, logEntries, d, lambdaEvent.RequestID) + if err != nil { + l.Error("[dispatcher:Dispatch] Failed to dispatch, returning to queue:", err) + for logEntry := range logEntries { + logEventsQueue.Put(logEntry) + } + } + } +} diff --git a/telemetryApi/listener.go b/telemetryApi/listener.go new file mode 100644 index 00000000..8a964d89 --- /dev/null +++ b/telemetryApi/listener.go @@ -0,0 +1,103 @@ +package telemetryApi + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/golang-collections/go-datastructures/queue" +) + +const defaultListenerPort = "4323" +const initialQueueSize = 5 + +// Used to listen to the Telemetry API +type TelemetryApiListener struct { + httpServer *http.Server + // LogEventsQueue is a synchronous queue and is used to put the received log events to be dispatched later + LogEventsQueue *queue.Queue +} + +func NewTelemetryApiListener() *TelemetryApiListener { + return &TelemetryApiListener{ + httpServer: nil, + LogEventsQueue: queue.New(initialQueueSize), + } +} + +func listenOnAddress() string { + env_aws_local, ok := os.LookupEnv("AWS_SAM_LOCAL") + var addr string + if ok && env_aws_local == "true" { + addr = ":" + defaultListenerPort + } else { + addr = "sandbox:" + defaultListenerPort + } + + return addr +} + +// Starts the server in a goroutine where the log events will be sent +func (s *TelemetryApiListener) Start() (string, error) { + address := listenOnAddress() + l.Debug("[listener:Start] Starting on address", address) + s.httpServer = &http.Server{Addr: address} + http.HandleFunc("/", s.http_handler) + go func() { + err := s.httpServer.ListenAndServe() + if err != http.ErrServerClosed { + l.Error("[listener:goroutine] Unexpected stop on Http Server:", err) + s.Shutdown() + } else { + l.Debug("[listener:goroutine] Http Server closed:", err) + } + }() + return fmt.Sprintf("http://%s/", address), nil +} + +// http_handler handles the requests coming from the Telemetry API. +// Everytime Telemetry API sends log events, this function will read them from the response body +// and put into a synchronous queue to be dispatched later. +// Logging or printing besides the error cases below is not recommended if you have subscribed to +// receive extension logs. Otherwise, logging here will cause Telemetry API to send new logs for +// the printed lines which may create an infinite loop. +func (s *TelemetryApiListener) http_handler(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + l.Error("[listener:http_handler] Error reading body:", err) + return + } + // Parse and put the log messages into the queue + var slice []LambdaTelemetryEvent + _ = json.Unmarshal(body, &slice) + + for _, el := range slice { + s.LogEventsQueue.Put(el) + } + + l.Debug("[listener:http_handler] logEvents received:", len(slice), " LogEventsQueue length:", s.LogEventsQueue.Len()) + slice = nil +} + +// Terminates the HTTP server listening for logs +func (s *TelemetryApiListener) Shutdown() { + if s.httpServer != nil { + ctx, _ := context.WithTimeout(context.Background(), 1*time.Second) + err := s.httpServer.Shutdown(ctx) + if err != nil { + l.Error("[listener:Shutdown] Failed to shutdown http server gracefully:", err) + } else { + s.httpServer = nil + } + } +} + +type LambdaTelemetryEvent struct { + Time string + Type string + Record any +} diff --git a/telemetryApi/send_to_new_relic.go b/telemetryApi/send_to_new_relic.go new file mode 100644 index 00000000..d3ea5adc --- /dev/null +++ b/telemetryApi/send_to_new_relic.go @@ -0,0 +1,435 @@ +package telemetryApi + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "reflect" + "strings" + "sync" + "time" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" + "github.com/pkg/errors" + + "github.com/google/uuid" + + "newrelic-lambda-extension/util" +) + +const ( + LogEndpointEU string = "https://log-api.eu.newrelic.com/log/v1" + LogEndpointUS string = "https://log-api.newrelic.com/log/v1" + + MetricsEndpointEU string = "https://metric-api.eu.newrelic.com/metric/v1" + MetricsEndpointUS string = "https://metric-api.newrelic.com/metric/v1" + + EventsEndpointEU string = "https://insights-collector.eu01.nr-data.net/v1/accounts/" + EventsEndpointUS string = "https://insights-collector.newrelic.com/v1/accounts/" + + TracesEndpointEU string = "https://trace-api.eu.newrelic.com/trace/v1" + TracesEndpointUS string = "https://trace-api.newrelic.com/trace/v1" + + maxLogMsgLen = 4094 + 10000 // maximum blob size + maxPayloadSizeBytes = 1000000 // 1 Mb + MaxAttributeValueLen = 4094 +) + +var ( + sess = session.Must(session.NewSessionWithOptions(session.Options{ + SharedConfigState: session.SharedConfigEnable, + })) + secrets secretsmanageriface.SecretsManagerAPI +) + +type licenseKeySecret struct { + LicenseKey string +} + +func init() { + secrets = secretsmanager.New(sess) +} + +func decodeLicenseKey(rawJson *string) (string, error) { + var lks licenseKeySecret + + err := json.Unmarshal([]byte(*rawJson), &lks) + if err != nil { + return "", err + } + if lks.LicenseKey == "" { + return "", fmt.Errorf("malformed license key secret; missing \"LicenseKey\" attribute") + } + + return lks.LicenseKey, nil +} + +func getNewRelicLicenseKey(ctx context.Context) (string, error) { + sId := "NEW_RELIC_LICENSE_KEY" + v := os.Getenv("NEW_RELIC_LICENSE_KEY_SECRET") + if len(v) > 0 { + sId = v + } + l.Debugf("fetching secret with name or ARN: %s", sId) + secretValueInput := secretsmanager.GetSecretValueInput{SecretId: &sId} + secretValueOutput, err := secrets.GetSecretValueWithContext(ctx, &secretValueInput) + if err != nil { + return "", err + } + return decodeLicenseKey(secretValueOutput.SecretString) +} + +func getEndpointURL(licenseKey string, typ string, EndpointOverride string) string { + if EndpointOverride != "" { + return EndpointOverride + } + switch typ { + case "logging": + if strings.HasPrefix(licenseKey, "eu") { + return LogEndpointEU + } else { + return LogEndpointUS + } + case "metrics": + if strings.HasPrefix(licenseKey, "eu") { + return MetricsEndpointEU + } else { + return MetricsEndpointUS + } + case "events": + if strings.HasPrefix(licenseKey, "eu") { + return EventsEndpointEU + } else { + return EventsEndpointUS + } + case "traces": + if strings.HasPrefix(licenseKey, "eu") { + return TracesEndpointEU + } else { + return TracesEndpointUS + } + } + return "" +} + +func buildPayloads(ctx context.Context, logEntries []interface{}, d *Dispatcher, RequestID string) (map[string][]map[string]interface{}, error) { + startBuild := time.Now() + extension_name := util.Name + + // NB "." is not allowed in NR eventType + var replacer = strings.NewReplacer(".", "_") + + data := make(map[string][]map[string]interface{}) + data["events"] = []map[string]interface{}{} + data["traces"] = []map[string]interface{}{} + data["logging"] = []map[string]interface{}{} + data["metrics"] = []map[string]interface{}{} + + // current logic - terminate processing on an error, can be changed later + for _, event := range logEntries { + msInt, err := time.Parse(time.RFC3339, event.(LambdaTelemetryEvent).Time) + if err != nil { + return nil, err + } + // events + data["events"] = append(data["events"], map[string]interface{}{ + "timestamp": msInt.UnixMilli(), + /*"plugin": util.Id, + "faas.arn": d.arn, + "faas.name": d.functionName, */ + "eventType": "TelemetryApiEvent", + "extension.name": extension_name, + "extension.version": util.Version, + "lambda.name": d.functionName, + "lambda.logevent.type": replacer.Replace(event.(LambdaTelemetryEvent).Type), + }) + // logs + if event.(LambdaTelemetryEvent).Record != nil { + msg := fmt.Sprint(event.(LambdaTelemetryEvent).Record) + if len(msg) > maxLogMsgLen { + msg = msg[:maxLogMsgLen] + } + data["logging"] = append(data["logging"], map[string]interface{}{ + "timestamp": msInt.UnixMilli(), + "message": msg, + "attributes": map[string]interface{}{ + "plugin": util.Id, + "entity": map[string]string{ + "name": d.functionName, + }, + "faas": map[string]string{ + "arn": d.arn, + "name": d.functionName, + }, + "aws": map[string]string{ + "lambda.logevent.type": event.(LambdaTelemetryEvent).Type, + "extension.name": extension_name, + "extension.version": util.Version, + "lambda.name": d.functionName, + "lambda.arn": d.arn, + "requestId": RequestID, + }, + }, + }) + + if reflect.ValueOf(event.(LambdaTelemetryEvent).Record).Kind() == reflect.Map { + eventRecord := event.(LambdaTelemetryEvent).Record.(map[string]interface{}) + // metrics + rid := "" + if v, okk := eventRecord["requestId"].(string); okk { + rid = v + } + if val, ok := eventRecord["metrics"].(map[string]interface{}); ok { + for key := range val { + data["metrics"] = append(data["metrics"], map[string]interface{}{ + "name": "aws.telemetry.lambda_ext." + key, + "value": val[key], + "timestamp": msInt.UnixMilli(), + "attributes": map[string]interface{}{ + "plugin": util.Id, + "faas.arn": d.arn, + "faas.name": d.functionName, + "lambda.logevent.type": event.(LambdaTelemetryEvent).Type, + "requestId": rid, + "extension.name": d.functionName, + "extension.version": util.Version, + "lambda.name": d.functionName, + }, + }) + } + } + // spans + if val, ok := eventRecord["spans"].([]interface{}); ok { + for _, span := range val { + attributes := make(map[string]interface{}) + attributes["event"] = event.(LambdaTelemetryEvent).Type + attributes["service.name"] = extension_name + var start time.Time + for key, v := range span.(map[string]interface{}) { + if key == "durationMs" { + attributes["duration.ms"] = v.(float64) + } else if key == "start" { + start, err = time.Parse(time.RFC3339, v.(string)) + if err != nil { + return nil, err + } + } else { + attributes[key] = v.(string) + } + } + el := map[string]interface{}{ + "trace.id": rid, + "timestamp": start.UnixMilli(), + "id": (uuid.New()).String(), + "attributes": attributes, + } + data["traces"] = append(data["traces"], el) + } + } + } + } + } + l.Debugf("[telemetryApi:buildPayloads] telemetry api payload objects built in: %s", time.Since(startBuild).String()) + return data, nil +} + +func marshalAndCompressData(d *Dispatcher, data []map[string]interface{}, dataType string) ([]*bytes.Buffer, error) { + bodyBytes, _ := json.Marshal(data) + + compressed, err := d.compressTool.Compress(bodyBytes) + if err != nil { + return nil, err + } + + if compressed.Len() > maxPayloadSizeBytes { + // Payload is too large, split in half, recursively + split := (len(data) / 2) + leftRet, err := marshalAndCompressData(d, data[0:split], dataType) + if err != nil { + return nil, err + } + + rightRet, err := marshalAndCompressData(d, data[split:], dataType) + if err != nil { + return nil, err + } + + return append(leftRet, rightRet...), nil + } + + return []*bytes.Buffer{compressed}, nil +} + +// please send compressed data +func sendData(ctx context.Context, d *Dispatcher, uri, dataType string, body *bytes.Buffer) error { + startSend := time.Now() + timeoutCtx, cancel := context.WithTimeout(ctx, util.SendToNewRelicTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(timeoutCtx, "POST", uri, body) + if err != nil { + return err + } + // the headers might be different for different endpoints + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Api-Key", d.licenseKey) + req.Header.Set("Content-Encoding", "GZIP") + if strings.Contains(uri, "trace") { + req.Header.Set("Data-Format", "newrelic") + req.Header.Set("Data-Format-Version", "1") + } + + res, err := d.httpClient.Do(req) + err = detecteErrorSendingBatch(res, err) + if err != nil { + l.Debugf("[telemetryApi:sendBatch] error occured while sending batch after %s", time.Since(startSend).String()) + return err + } + + l.Debugf("[telemetryApi:sendBatch] took %s to send %s json", time.Since(startSend).String(), dataType) + return err +} + +func detecteErrorSendingBatch(response *http.Response, err error) error { + if err != nil { + return fmt.Errorf("[telemetryApi:sendBatch] Telemetry client error: %s", err) + } else if response.StatusCode >= 300 { + bytes, err := io.ReadAll(response.Body) + if err != nil { + return err + } + return fmt.Errorf("[telemtryApi:sendBatch] Telemetry client response: [%s] %s", response.Status, fmt.Sprint(bytes)) + } + return nil +} + +func sendDataToNR(ctx context.Context, logEntries []interface{}, d *Dispatcher, RequestID string) error { + data, err := buildPayloads(ctx, logEntries, d, RequestID) + if err != nil { + return err + } + + if len(data) > 0 { + waitForSend := sync.WaitGroup{} + errChan := make(chan error, 4) + startAggregateSend := time.Now() + + // send logs + if len(data["logging"]) > 0 { + logPayloads, err := marshalAndCompressData(d, data["logging"], "logging") + if err != nil { + return err + } + + for _, logPayload := range logPayloads { + l.Debugf("[telemetryApi:sendDataToNR] sending %d compressed log payloads to new relic", len(logPayloads)) + waitForSend.Add(len(logPayloads)) + go func(logPayload *bytes.Buffer) { + defer waitForSend.Done() + err := sendData(ctx, d, getEndpointURL(d.licenseKey, "logging", ""), "logging", logPayload) + if err != nil { + errChan <- err + } + }(logPayload) + } + } + // send metrics + if len(data["metrics"]) > 0 { + waitForSend.Add(1) + startMarshal := time.Now() + var dataMet []map[string][]map[string]interface{} + dataMet = append(dataMet, map[string][]map[string]interface{}{ + "metrics": data["metrics"], + }) + bodyBytes, _ := json.Marshal(dataMet) + l.Debugf("[telemetryApi:sendDataToNR] took %s to marshal metrics json with %d metrics", time.Since(startMarshal).String(), len(data["metrics"])) + l.Tracef("[telemetryApi:sendDataToNR] Metrics JSON: %s", string(bodyBytes)) + + metricsPayload, err := d.compressTool.Compress(bodyBytes) + if err != nil { + return err + } + + go func() { + defer waitForSend.Done() + err := sendData(ctx, d, getEndpointURL(d.licenseKey, "metrics", ""), "metrics", metricsPayload) + if err != nil { + errChan <- err + } + }() + } + // send events + if len(data["events"]) > 0 { + if len(d.accountID) > 0 { + eventPayloads, err := marshalAndCompressData(d, data["events"], "logging") + if err != nil { + return err + } + + l.Debugf("[telemetryApi:sendDataToNR] sending %d compressed log payloads to new relic", len(eventPayloads)) + waitForSend.Add(len(eventPayloads)) + for _, eventPayload := range eventPayloads { + go func(eventPayload *bytes.Buffer) { + defer waitForSend.Done() + err := sendData(ctx, d, getEndpointURL(d.licenseKey, "events", "")+d.accountID+"/events", "events", eventPayload) + if err != nil { + errChan <- err + } + }(eventPayload) + } + } else { + l.Warn("[telemetryApi:sendDataToNR] NEW_RELIC_ACCOUNT_ID is not set, therefore no events data sent") + } + } + // send traces + if len(data["traces"]) > 0 { + waitForSend.Add(1) + var dataTraces []map[string]interface{} + dataTraces = append(dataTraces, map[string]interface{}{ + "common": map[string]map[string]string{ + "attributes": { + "host": "aws.amazon.com", + "service.name": d.functionName, + }, + }, + "spans": data["traces"], + }) + startMarshal := time.Now() + bodyBytes, _ := json.Marshal(dataTraces) + l.Debugf("[telemetryApi:sendDataToNR] took %s to marshal traces json with %d traces", time.Since(startMarshal).String(), len(data["traces"])) + l.Tracef("[telemetryApi:sendDataToNR] Traces JSON: %s", string(bodyBytes)) + + tracePayload, err := d.compressTool.Compress(bodyBytes) + + go func() { + defer waitForSend.Done() + er := sendData(ctx, d, getEndpointURL(d.licenseKey, "traces", ""), "traces", tracePayload) + if er != nil { + errChan <- err + } + }() + + } + + l.Debugf("[telemetryApi:sendDataToNR] waiting for all payloads to send to new relic...") + waitForSend.Wait() + l.Debugf("[telemetryApi:sendDataToNR] waited %s for all Telemetry API payloads to concurrently send to new relic", time.Since(startAggregateSend).String()) + + if len(errChan) > 0 { + err = fmt.Errorf("%d errors occured while sending telemetry API payloads to New Relic", len(errChan)) + for i := 0; i < len(errChan); i++ { + sendError := <-errChan + errors.Wrap(err, sendError.Error()) + } + return err + } + } + + return nil // success +} diff --git a/telemetryApi/send_to_new_relic_test.go b/telemetryApi/send_to_new_relic_test.go new file mode 100644 index 00000000..ee413120 --- /dev/null +++ b/telemetryApi/send_to_new_relic_test.go @@ -0,0 +1,729 @@ +package telemetryApi + +import ( + "fmt" + "newrelic-lambda-extension/util" + "testing" +) + +const ( + kilobyte = ` 54 154 35 166 28 69 158 222 130 190 36 106 21 29 200 210 + 161 194 45 184 203 185 89 208 24 223 32 147 78 213 161 251 + 129 227 145 251 215 92 135 230 166 134 46 91 37 54 111 174 + 103 7 7 205 56 155 149 98 9 182 249 148 163 178 176 108 + 251 247 85 243 4 230 65 177 50 24 97 121 133 251 121 209 + 162 112 140 254 253 192 171 13 74 186 237 34 178 80 238 25 + 32 234 37 218 94 169 48 182 200 122 236 205 166 239 230 217 + 90 35 157 72 157 21 247 124 198 93 90 19 230 129 36 31 + 201 1 48 40 241 11 63 11 238 162 212 247 93 128 232 134 + 184 161 111 66 97 172 23 230 175 32 135 77 115 141 33 212 + 230 224 227 204 179 56 39 82 225 2 246 191 104 174 154 59 + 161 149 9 62 141 86 233 194 192 42 77 201 200 70 95 107 + 118 37 142 24 238 181 28 244 119 163 252 113 238 37 30 16 + 49 234 190 101 64 18 245 233 232 79 242 207 146 159 209 174 + 18 16 193 159 157 111 201 252 227 171 248 57 4 40 44 124 + 37 248 92 133 167 179 65 104 222 222 36 102 203 166 175 137 + 197 76 57 168 157 184 9 226 198 162 107 248 106 43 226 93 + 94 244 59 54 95 110 202 82 173 205 81 162 253 166 55 136 + 93 155 216 163 27 121 40 79 228 212 200 164 27 128 104 120 + 41 75 42 243 198 219 51 89 119 12 192 94 102 106 30 204 + 4 133 197 50 206 128 36 238 167 38 161 13 170 190 235 29 + 255 155 240 222 252 41 44 74 144 171 253 227 236 211 15 175 + 2 121 197 88 9 52 40 34 92 96 179 83 230 237 106 253 + 112 190 213 184 129 158 205 223 118 206 188 117 80 222 199 210 + 129 251 49 131 7 76 201 117 211 224 34 84 227 226 50 159 + 93 110 28 229 203 91 76 157 60 201 89 149 244 59 213 234 + 195 130 121 139 82 60 186 130 125 93 84 34 182 227 131 102 + 32 28 233 61 19 197 223 87 40 197 250 201 114 157 194 155 + 242 159 154 27 138 83 129 215 235 126 160 115 107 111 123 14 + 252 252 210 111 130 161 248 68 251 32 76 173 110 252 72 95 + 7 204 98 62 63 0 232 190 34 147 149 195 166 139 44 244 + 232 23 16 39 14 24 50 191 207 220 78 213 45 88 2 138 + 74 216 188 237 83 7 229 20 205 57 197 69 85 174 145 29 + 181 254 93 143 250 165 127 128 121 103 112 54 77 219 45 82 + 31 87 136 55 78 80 230 222 177 224 100 245 233 234 95 46 + 88 126 79 234 138 215 215 99 85 132 3 59 121 99 144 202 + 88 78 84 206 146 227 211 133 244 29 36 208 147 206 51 88 + 197 249 12 70 252 185 50 93 137 230 28 240 90 35 65 241 + 136 25 193 165 108 85 96 103 128 33 102 192 60 90 188 228 + 163 213 12 183 43 38 57 3 72 238 122 101 153 217 18 56 + 246 48 88 101 116 27 125 114 27 93 30 135 88 138 112 124 + 112 222 222 52 102 130 129 24 167 71 39 239 105 240 43 249 + 144 148 65 223 25 28 79 241 146 16 99 35 184 23 207 243 + 63 192 204 44 85 26 46 135 170 200 29 9 237 248 66 186 + 94 88 216 170 247 157 238 54 157 163 129 42 121 164 149 19 + 204 81 29 248 74 73 192 85 78 105 149 118 43 18 88 15 + 51 31 110 243 174 153 121 196 166 17 127 181 0 237 108 53 + 9 0 199 39 61 77 191 146 127 163 82 141 172 239 210 31 + 47 96 108 53 134 98 120 62 93 195 133 86 26 160 156 238 + 22 32 251 16 118 161 4 160 233 75 73 151 238 217 107 73 + 208 223 110 249 203 184 201 169 74 93 106 42 160 121 190 254 + 166 5 245 123 22 170 236 28 173 114 109 79 0 157 248 57 + 176 246 25 153 82 245 2 41 4 217 233 31 143 120 164 194 + 63 117 9 105 190 212 53 116 36 30 247 5 2 208 135 6 + 96 157 90 3 206 220 239 31 248 121 197 64 104 160 42 222 + 83 165 133 236 27 45 94 1 80 147 152 55 249 143 136 210 + 179 186 77 22 97 78 146 54 204 120 218 131 26 20 10 52 + 238 5 212 219 128 17 106 158 47 68 149 105 202 158 137 154 + 240 252 29 94 98 18 119 64 198 31 177 7 88 74 227 215 + 189 180 127 58 60 171 44 65 62 224 177 155 140 204 169 160 + 30 155 72 26 144 217 161 90 66 108 210 177 255 185 139 136 + 156 138 75 12 235 173 156 114 238 114 254 201 149 3 26 25 + 211 20 63 81 146 247 27 75 233 7 169 253 141 87 133 61 + 99 229 131 135 177 101 34 48 218 10 255 76 152 62 98 27 + 99 169 56 157 214 166 14 157 235 130 113 63 0 46 232 36 + 218 139 115 166 243 12 144 202 117 129 107 79 161 38 59 139 + 238 27 95 111 72 211 239 228 123 212 101 249 240 167 19 114 + 229 5 200 137 118 53 97 41 81 186 53 68 35 90 50 140 + 122 96 156 86 78 214 106 105 99 81 9 141 106 97 136 254 + 220 18 208 74 113 254 165 89 19 14 92 213 134 207 54 49 + 93 29 211 221 125 60 34 151 60 179 175 10 103 132 82 143 + 43 190 95 96 54 249 162 244 165 111 34 215 218 225 94 48 + 24 241 117 235 105 70 94 140 249 110 180 147 35 124 118 90 + 122 93 216 100 128 64 14 56 98 16 38 4 214 57 250 68 + 151 104 51 197 152 210 59 8 180 126 230 138 237 11 195 188 + 233 247 47 190 174 64 144 19 146 67 202 97 151 197 228 122 + 200 179 141 106 129 15 102 72 248 82 113 229 103 27 241 234 + 221 75 156 5 252 104 16 250 46 236 99 70 75 253 31 170 + 110 240 197 167 253 174 243 60 15 118 7 248 148 83 103 213 + 163 89 171 245 187 221 157 208 90 2 197 75 114 110 98 30 + 219 5 176 15 184 85 182 206 18 3 10 254 44 171 138 113 + 160 3 219 161 207 237 174 173 100 211 192 31 71 24 217 236 + 222 179 201 120 128 224 83 133 196 13 125 30 246 60 200 84 + 142 225 200 192 155 57 227 122 14 203 173 125 161 123 190 68 + 179 100 170 218 216 194 65 179 237 104 218 31 82 226 180 134 + 145 154 249 232 229 12 241 148 103 164 23 74 97 171 90 63 + 6 134 235 171 220 40 162 182 12 224 27 141 201 252 142 105 + 93 156 46 114 133 102 248 203 98 81 156 123 186 196 127 133 + 213 80 253 119 164 170 159 33 217 77 90 12 36 115 49 20 + 230 219 205 144 96 201 19 190 130 0 255 0 233 133 148 131 + 102 238 192 164 213 197 125 55 174 7 231 215 5 231 244 236 + 151 9 246 218 177 69 201 250 149 7 209 130 188 2 171 232 + 32 244 182 91 197 125 249 92 145 9 233 16 9 229 0 194 + 158 120 201 52 161 223 175 143 214 123 236 3 8 7 229 154 + 68 206 16 62 114 3 97 145 245 242 209 55 101 116 31 38 + 21 13 176 74 123 183 51 103 68 225 146 180 155 119 239 13 + 222 177 134 227 181 88 15 215 110 234 182 89 69 58 97 68 + 59 50 165 49 238 142 74 250 123 183 174 240 49 177 120 48 + 177 76 107 57 216 145 103 127 66 41 189 127 229 235 45 17 + 11 68 232 55 154 169 21 191 20 250 54 197 113 193 144 29 + 109 156 210 33 112 159 231 116 235 161 180 229 178 225 15 80 + 151 117 252 115 115 5 186 49 97 6 30 162 188 103 208 164 + 230 48 117 230 144 226 5 90 46 116 254 6 61 104 138 42 + 6 87 255 141 103 203 36 126 20 31 244 255 182 177 243 128 + 238 37 226 76 247 37 36 169 168 15 122 239 13 194 49 245 + 44 164 196 77 183 133 238 13 241 7 167 82 240 122 58 49 + 198 102 40 215 76 132 232 95 170 160 179 31 201 247 210 80 + 81 208 175 207 73 252 43 229 225 215 189 244 60 226 84 96 + 76 151 48 154 255 106 190 200 238 70 126 189 101 128 157 68 + 193 131 198 113 251 105 202 176 125 1 47 77 209 240 82 148 + 2 75 131 239 204 231 174 222 190 210 140 168 118 85 70 87 + 81 57 209 201 15 235 131 61 24 223 166 151 133 148 136 56 + 216 30 161 107 140 213 48 30 39 238 123 164 126 229 109 250 + 255 75 32 223 17 95 94 132 95 229 168 80 85 8 144 13 + 25 56 217 246 25 87 182 89 39 44 128 61 20 57 98 183 + 252 31 118 30 12 243 149 252 90 213 6 183 119 81 127 232 + 53 62 255 203 185 134 106 208 136 67 5 75 62 193 57 108 + 44 102 17 91 170 134 230 20 38 39 138 227 202 59 7 26 + 239 111 192 133 115 113 89 159 223 185 216 239 253 3 254 114 + 31 190 107 200 53 149 118 5 57 241 40 241 45 172 15 215 + 16 101 47 60 101 213 182 69 75 58 69 224 248 188 183 75 + 5 26 162 234 245 121 126 208 98 223 113 204 84 64 190 28 + 75 236 248 71 249 98 9 116 47 39 72 228 146 31 173 144 + 205 82 159 134 93 72 254 13 118 32 16 253 8 79 167 94 + 181 140 158 67 107 15 118 232 171 239 172 155 209 27 115 21 + 104 50 26 83 211 145 170 18 35 225 170 129 253 119 69 244 + 174 29 231 85 15 54 153 119 122 74 157 174 98 246 174 40 + 177 49 15 154 145 108 111 86 140 246 93 99 255 28 51 129 + 6 112 74 131 32 77 36 191 213 98 63 211 101 167 136 164 + 200 157 104 38 251 22 96 86 79 197 153 172 49 2 70 64 + 13 173 53 118 183 185 30 135 60 136 158 116 22 233 148 51 + 52 252 205 216 147 210 218 23 117 23 95 79 220 110 53 242 + 156 103 54 97 181 202 216 202 151 5 66 47 82 248 237 251 + 35 194 75 26 252 165 244 165 125 254 124 244 116 42 24 11 + 223 33 153 216 5 62 51 76 80 171 80 76 101 227 123 192 + 236 216 255 101 178 97 218 199 236 229 33 245 49 180 247 86 + 239 52 107 43 67 139 171 75 177 40 249 255 55 191 169 116 + 137 172 150 19 146 135 41 121 158 241 82 115 184 5 229 253 + 104 159 233 94 76 19 219 41 216 73 222 194 190 133 180 21 + 55 60 57 28 179 230 148 164 226 197 100 114 44 67 76 236 + 88 76 99 72 42 124 216 124 92 53 184 140 131 50 175 187 + 46 244 144 198 211 187 10 207 93 237 113 213 67 54 178 148 + 150 74 242 98 9 133 42 187 47 59 230 184 119 126 128 239 + 51 198 3 230 201 81 246 253 0 218 50 81 163 23 245 54 + 141 77 110 205 111 3 158 146 15 206 154 215 138 5 217 26 + 216 59 5 173 185 130 208 83 225 141 85 178 166 206 67 155 + 165 47 244 39 29 61 165 151 254 253 144 84 71 40 160 158 + 72 52 15 182 180 34 85 30 84 242 176 162 165 143 137 94 + 231 68 176 115 9 18 83 36 134 146 84 247 181 85 110 164 + 198 197 65 221 50 134 108 12 59 184 156 79 161 153 152 78 + 167 158 56 248 83 169 33 30 80 16 97 63 159 170 73 54 + 153 244 78 153 54 67 25 48 56 140 27 73 164 211 142 226 + 75 58 30 49 31 232 22 61 53 141 161 97 244 98 81 158 + 150 93 147 146 229 201 12 198 10 251 142 25 210 109 44 168 + 237 174 88 227 236 117 7 84 105 21 227 19 213 235 235 209 + 211 99 61 186 103 179 87 9 3 234 125 62 106 253 33 195 + 72 37 81 196 77 122 27 104 170 171 65 176 179 251 82 102 + 43 154 167 137 145 76 44 93 215 144 249 104 135 184 251 178 + 230 199 85 40 128 91 120 232 197 126 14 231 135 119 167 74 + 39 157 0 244 135 13 103 123 40 242 145 13 54 248 30 66 + 172 138 195 225 134 150 51 176 233 216 175 18 112 99 192 198 + 232 93 96 135 91 242 71 223 189 126 120 173 120 232 145 33 + 90 43 112 175 178 30 190 208 22 251 208 53 220 123 253 199 + 166 203 172 24 145 222 144 164 11 198 136 169 54 112 74 13 + 38 152 177 11 5 139 17 172 223 211 21 148 153 78 34 23 + 46 18 166 235 154 115 30 67 192 207 95 205 216 231 139 215 + 14 34 237 237 110 40 127 234 136 68 57 223 159 46 80 46 + 0 67 0 193 253 42 160 221 153 137 74 247 59 254 130 96 + 7 222 237 54 182 230 40 98 121 208 129 3 135 199 190 72 + 86 182 30 37 102 166 175 43 3 219 134 61 176 74 225 22 + 72 147 235 134 133 50 214 217 90 99 164 181 43 4 146 37 + 135 83 4 235 229 19 32 168 235 188 40 159 151 184 156 171 + 67 203 179 128 250 26 241 21 217 221 131 47 71 1 169 120 + 21 184 72 217 17 196 145 134 220 219 38 113 109 30 221 165 + 198 55 253 6 67 220 149 124 155 160 92 216 184 41 73 136 + 49 186 32 34 44 7 210 166 188 150 60 245 211 153 117 126 + 60 124 193 157 241 75 253 204 209 97 45 45 68 86 220 5 + 113 18 44 96 34 207 2 26 85 124 28 19 181 122 233 197 + 190 250 245 200 129 142 26 223 120 99 36 111 12 226 255 223 + 52 29 54 164 162 4 153 42 66 240 236 3 103 111 224 79 + 227 91 148 228 219 118 188 189 167 248 21 24 2 36 119 244 + 147 112 142 3 126 245 64 129 223 106 61 36 233 55 130 255 + 132 75 126 179 88 88 2 57 235 95 185 33 98 242 73 26 + 178 103 34 124 93 20 60 214 43 153 104 247 167 44 186 177 + 224 53 78 14 112 244 121 93 59 164 55 194 242 253 238 161 + 164 77 21 38 54 200 114 82 187 161 152 143 2 144 150 194 + 104 41 87 107 159 1 197 169 56 137 81 212 48 148 157 181 + 46 240 227 150 176 50 172 253 232 80 162 177 216 231 136 254 + 78 4 82 44 240 207 208 11 117 66 166 93 185 255 250 56 + 171 114 224 81 10 69 193 181 185 54 201 234 26 229 203 197 + 131 5 42 126 245 88 41 130 251 56 191 109 74 236 174 76 + 146 115 103 56 110 75 244 60 191 54 167 174 84 151 215 107 + 140 201 64 4 163 105 219 62 87 6 217 2 236 101 99 20 + 54 35 96 237 48 11 78 122 90 105 221 113 47 117 132 28 + 118 96 136 216 117 76 214 202 58 85 234 63 224 36 46 16 + 158 18 187 112 163 188 129 36 90 123 80 253 253 15 63 109 + 161 231 76 79 212 45 144 89 64 229 187 12 134 224 91 190 + 75 142 80 53 196 87 221 125 162 67 37 218 239 91 111 236 + 217 68 204 59 148 225 168 211 217 63 112 224 83 36 89 191 + 139 219 208 118 67 90 31 71 3 202 131 48 226 103 125 150 + 50 119 70 243 152 121 24 84 210 26 46 58 140 0 27 22 + 159 185 158 6 69 165 185 250 17 224 100 93 140 143 59 110 + 151 193 38 98 159 245 254 33 126 27 49 147 247 48 49 107 + 42 93 210 154 61 133 142 230 233 78 121 36 21 187 33 184 + 70 235 53 67 25 210 5 147 222 97 36 227 242 161 248 105 + 244 129 122 148 151 58 105 43 177 176 188 67 67 184 75 130 + 255 49 115 59 195 0 50 128 59 183 146 253 231 69 0 252 + 168 171 153 215 233 151 29 234 9 63 69 111 188 78 13 233 + 135 58 173 95 81 19 101 163 145 7 100 179 1 254 212 41 + 208 178 32 88 2 152 57 183 103 247 96 20 89 166 207 196 + 206 65 161 174 3 142 211 54 151 73 167 93 105 132 69 244 + 45 50 105 237 217 119 107 211 234 173 82 120 124 201 161 255 + 234 146 99 243 193 180 155 101 90 97 159 150 96 10 153 90 + 237 180 182 33 221 82 69 136 65 243 158 137 234 202 92 157 + 47 218 221 153 26 132 113 198 77 175 227 28 243 56 120 10 + 204 203 196 87 164 155 253 149 254 24 31 35 171 146 10 230 + 193 166 226 76 94 89 219 77 136 17 53 125 21 83 65 6 + 59 17 248 118 156 99 67 81 113 94 146 239 92 18 139 195 + 51 78 194 54 113 12 150 219 199 195 141 105 111 217 253 31 + 100 217 93 108 30 15 38 108 40 62 13 153 83 214 15 86 + 34 105 4 15 219 112 113 222 89 177 142 208 160 12 48 224 + 79 85 220 3 89 210 232 217 250 207 222 228 119 67 44 180 + 116 90 65 84 124 61 124 43 62 82 90 100 79 146 131 121 + 225 20 131 48 118 141 187 126 35 154 148 165 224 176 198 209 + 143 242 21 14 111 129 27 193 229 168 122 201 146 232 224 139 + 78 189 82 224 180 143 205 125 202 162 165 192 139 172 35 162 + 116 221 168 103 150 114 153 118 148 20 127 245 49 240 26 120 + 50 175 241 37 67 140 23 227 159 8 119 208 243 247 238 240 + 71 87 9 33 204 195 164 2 36 158 127 211 178 1 190 76 + 151 202 169 166 75 52 16 234 207 233 15 26 167 61 25 90 + 206 205 205 42 68 218 91 7 197 51 23 177 129 11 226 207 + 67 239 196 49 92 221 70 105 237 107 40 128 22 216 201 183 + 32 99 56 110 58 175 193 98 26 239 53 61 135 41 99 181 + 162 69 53 222 73 32 83 50 120 197 199 1 15 224 183 79 + 45 153 97 7 172 253 14 193 151 247 204 153 1 227 220 245 + 242 99 143 75 180 21 47 132 44 203 207 154 140 248 80 54 + 223 206 83 243 130 250 242 245 177 208 100 207 95 233 77 197 + 240 44 39 161 25 158 134 168 6 102 234 135 30 2 42 63 + 87 70 168 175 3 207 235 159 80 61 156 72 110 131 231 86 + 180 198 137 154 198 232 14 36 12 56 116 144 143 84 179 6 + 19 19 89 249 212 191 121 183 122 247 224 121 105 184 79 26 + 120 217 92 125 151 163 236 227 48 94 89 245 217 215 253 130 + 94 239 167 169 97 35 28 29 88 12 115 97 111 187 253 197 + 129 107 191 39 192 136 212 16 175 138 174 117 171 136 38 2 + 203 25 142 130 21 33 194 39 51 208 11 123 152 51 41 207 + 39 41 20 120 211 37 152 117 237 5 74 170 17 170 45 120 + 24 220 120 139 152 227 28 62 233 214 219 132 155 57 255 70 + 248 32 82 105 181 87 176 3 196 62 102 60 67 205 102 88 + 211 72 128 242 119 110 233 22 140 79 65 83 255 151 71 196 + 150 187 0 70 54 153 211 130 180 209 219 68 7 27 141 226 + 215 114 124 13 54 62 30 105 252 80 19 110 19 50 78 30 + 6 179 211 27 8 31 231 141 188 238 184 8 176 239 51 93 + 175 174 128 16 80 218 44 41 243 1 130 57 204 147 64 214 + 172 200 131 99 191 87 226 132 159 194 49 173 74 239 187 116 + 125 91 175 118 177 51 88 210 36 147 34 197 98 240 40 158 + 25 44 150 243 70 241 218 42 90 63 211 99 45 211 119 223 + 166 171 114 122 57 50 6 165 139 44 169 245 137 104 70 5 + 101 35 180 252 73 71 127 49 245 127 240 249 21 211 231 77 + 221 143 130 27 83 130 201 166 196 233 145 58 245 110 225 19 + 124 195 183 75 84 120 96 51 8 227 0 194 95 134 179 96 + 218 125 69 25 162 215 245 46 109 47 111 34 170 60 55 230 + 233 42 49 164 196 64 165 40 84 42 197 124 186 67 117 7 + 156 237 118 124 104 63 231 81 98 212 102 101 55 10 110 85 + 222 142 74 128 185 214 172 117 97 180 42 1 112 185 122 115 + 177 29 79 182 141 31 240 108 171 56 7 130 152 136 236 102 + 248 82 242 118 187 231 19 83 226 90 84 86 233 7 130 194 + 119 52 40 228 142 203 118 17 102 182 106 71 130 82 41 195 + 14 29 143 186 212 124 55 78 169 122 122 247 101 20 137 81 + 86 135 110 72 134 139 53 220 116 243 233 38 138 24 187 44 + 62 81 246 182 112 175 49 174 137 81 200 143 94 135 180 233 + 150 78 13 66 3 139 111 119 49 220 60 242 99 173 180 185 + 5 40 36 124 50 136 178 3 163 58 238 146 211 78 166 27 + 210 223 162 206 77 176 5 231 111 108 143 182 85 144 253 97 + 66 27 97 11 122 66 12 96 108 167 148 235 217 31 95 47 + 59 198 163 199 21 249 230 231 32 144 79 107 242 236 230 206 + 113 245 42 174 45 67 89 229 232 94 190 40 46 145 46 132 + 70 106 97 190 208 21 49 38 179 129 214 28 97 108 172 219 + 241 51 126 167 225 8 169 89 64 139 26 71 92 43 56 202 + 176 200 109 119 202 96 77 250 189 90 37 11 160 157 169 248 + 178 140 15 53 52 186 218 95 12 99 29 236 111 121 240 193 + 89 161 116 238 179 110 118 77 220 149 46 190 172 3 41 123 + 149 241 80 111 205 201 85 180 66 81 135 27 123 231 185 245 + 102 235 94 242 164 207 244 29 153 30 147 107 80 106 164 109 + 209 96 20 124 39 242 1 96 76 197 210 68 208 22 67 219 + 216 140 66 255 40 123 193 250 199 122 120 63 32 3 82 151 + 25 237 219 54 43 182 16 25 172 200 140 82 176 238 126 252 + 68 177 6 18 7 63 235 159 68 107 14 141 145 133 44 199 + 216 169 32 16 197 21 203 255 34 95 207 113 27 22 201 189 + 3 236 158 11 117 132 64 105 123 178 73 231 113 26 205 167 + 241 71 37 35 184 203 217 239 193 31 115 241 179 235 162 16 + 34 84 29 39 132 61 201 47 230 90 46 234 67 54 159 146 + 175 148 168 172 210 214 62 48 85 250 28 1 199 142 223 232 + 95 114 151 216 85 212 28 137 87 195 189 162 157 32 41 204 + 137 94 162 190 109 41 29 77 232 192 86 225 89 42 118 150 + 234 5 108 72 80 83 76 219 128 228 253 130 3 126 170 187 + 10 158 92 146 71 209 200 46 131 190 17 5 250 20 54 61 + 36 159 214 208 45 216 59 154 231 223 226 248 142 114 31 86 + 107 194 100 92 70 146 224 184 166 139 120 206 253 178 171 194 + 216 177 236 210 205 77 1 160 144 114 0 169 142 192 40 33 + 217 249 188 244 89 187 159 252 78 174 122 86 61 98 111 140 + 139 90 203 213 76 228 4 240 133 130 83 219 92 151 91 48 + 55 106 170 252 113 210 58 110 21 179 131 44 50 84 123 180 + 119 171 199 126 251 170 97 248 136 35 35 190 103 222 29 73 + 196 80 48 204 122 18 243 161 95 124 18 194 90 179 81 222 + 55 42 77 171 94 197 97 144 74 167 216 196 144 43 127 147 + 32 160 53 42 145 59 140 113 161 32 162 156 166 93 146 141 + 42 75 80 145 163 102 244 234 255 205 22 148 21 34 16 64 + 75 196 253 200 96 41 129 14 101 222 193 186 105 212 150 181 + 12 81 230 60 8 161 132 28 118 221 126 188 209 218 128 105 + 126 233 0 219 191 172 39 197 131 63 246 178 104 104 36 246 + 242 11 132 175 222 255 174 166 23 67 51 61 210 70 117 87 + 238 214 187 131 226 139 169 201 185 67 43 236 1 221 94 167 + 85 51 85 101 5 43 109 38 52 1 151 76 58 230 138 48 + 55 225 13 178 166 212 141 149 69 112 36 93 121 154 225 174 + 84 198 98 239 175 247 236 53 12 182 29 124 183 34 164 32 + 67 174 154 29 71 246 77 248 51 235 175 240 10 178 101 214 + 56 250 149 136 57 110 101 21 91 28 139 132 219 203 41 243 + 124 123 141 215 184 236 206 233 20 38 42 112 102 251 205 40 + 123 168 127 94 219 117 67 129 173 103 223 7 232 248 105 21 + 171 181 210 38 95 236 200 203 85 186 200 61 194 73 61 70 + 216 81 107 76 207 196 156 219 54 80 200 223 117 14 94 158 + 252 240 169 184 65 115 66 53 202 148 9 83 176 163 250 10 + 176 145 90 196 180 140 221 190 10 148 97 43 110 75 32 61 + 204 97 82 8 147 0 102 52 235 35 177 83 80 68 69 33 + 18 163 84 213 75 154 143 224 44 134 132 241 155 38 188 220 + 235 237 228 12 140 255 195 225 43 176 168 61 12 185 219 26 + 18 159 121 213 138 98 164 240 45 125 94 207 245 204 175 50 + 59 235 224 114 30 224 189 235 239 17 231 56 124 128 232 111 + 34 111 78 228 12 189 123 106 11 80 74 207 63 218 169 45 + 18 220 254 78 55 44 24 222 127 186 74 239 151 223 68 169 + 185 231 47 246 109 193 35 187 121 196 225 131 78 81 178 214 + 188 239 70 89 163 22 13 217 53 203 195 214 153 194 15 116 + 204 221 26 225 73 254 54 103 59 121 136 232 109 111 88 10 + 252 228 129 118 127 181 83 112 248 11 30 254 180 255 219 242 + 44 197 66 222 166 16 212 53 47 190 183 31 125 156 227 147 + 120 148 99 113 134 194 219 102 115 177 140 242 216 245 176 160 + 231 8 176 12 60 183 254 21 169 25 205 4 194 179 48 211 + 0 22 228 202 187 171 1 120 76 154 237 243 180 89 63 192 + 158 248 160 15 108 69 92 124 174 196 251 161 64 56 212 33 + 153 252 225 126 113 198 6 240 177 68 90 241 188 139 198 213 + 233 12 142 174 94 159 156 194 191 81 228 66 138 124 23 54 + 36 4 86 52 4 127 64 245 97 20 11 218 40 21 150 34 + 96 239 39 26 178 110 202 143 154 72 163 250 187 143 38 81 + 137 188 25 219 21 77 6 6 39 73 136 104 186 132 21 91 + 9 233 59 24 55 220 192 25 185 83 117 177 48 211 184 136 + 190 13 215 221 119 195 111 60 233 146 92 39 74 105 143 230 + 161 252 136 147 60 230 222 19 109 187 29 222 200 185 161 70 + 238 132 110 0 22 222 137 184 40 236 22 18 179 190 56 76 + 83 212 220 209 230 13 246 46 190 185 13 45 220 137 76 53 + 247 80 132 234 160 101 47 164 69 167 247 165 183 25 88 162 + 137 151 100 28 180 114 243 64 13 45 255 24 62 52 34 73 + 194 232 16 98 179 245 121 192 198 138 87 107 83 188 221 24 + 152 4 82 23 2 30 234 185 153 197 199 218 193 163 5 19 + 111 213 228 63 250 18 132 26 132 130 80 18 187 24 104 111 + 205 92 199 76 233 116 246 219 29 236 33 10 243 134 133 162 + 220 145 172 12 118 48 18 9 204 112 103 144 108 71 178 211 + 113 132 34 46 81 211 230 66 126 237 101 214 229 41 217 111 + 55 131 116 190 140 47 153 117 114 218 158 49 26 94 248 76 + 156 41 204 25 135 103 68 161 108 94 78 131 62 144 17 168 + 149 223 221 22 160 161 71 253 224 5 64 249 223 144 205 94 + 232 39 237 15 229 184 70 14 181 164 166 114 177 35 186 226 + 187 169 149 172 191 2 134 186 49 49 1 9 101 26 116 180 + 91 125 231 249 62 15 23 143 177 121 24 116 171 243 159 179 + 223 96 104 137 245 234 1 232 197 50 235 143 233 197 90 58 + 255 207 68 205 78 171 46 224 166 129 245 199 5 119 148 23 + 47 94 219 217 252 17 164 89 26 45 243 112 11 43 112 177 + 217 41 167 37 25 4 34 25 131 122 247 49 79 172 162 49 + 26 107 200 38 119 36 228 73 64 228 3 143 61 191 199 251 + 107 217 52 182 194 164 67 31 195 103 108 135 30 184 213 217 + 30 61 189 36 191 121 89 61 35 23 162 243 145 62 28 51 + 29 99 216 89 33 226 60 226 248 116 136 18 98 161 184 251 + 183 123 11 35 106 29 48 99 107 113 69 162 4 196 66 189 + 85 55 76 237 201 150 45 197 107 188 251 213 217 249 194 27 + 122 173 147 243 50 76 27 119 239 214 29 38 239 180 65 74 + 227 146 224 217 215 116 7 224 187 168 15 150 222 63 206 38 + 167 180 24 38 169 124 112 197 125 3 145 244 31 84 215 110 + 209 209 236 11 87 70 44 44 90 212 159 23 189 227 244 113 + 241 6 237 5 213 29 103 40 118 23 190 11 58 45 208 5 + 154 39 34 246 153 248 97 249 191 63 189 159 209 111 140 13 + 102 166 54 87 67 228 63 237 202 12 198 207 104 123 55 243 + 16 98 188 6 156 106 164 90 185 223 58 136 90 179 15 204 + 233 243 153 191 49 203 218 19 101 181 203 120 128 250 237 143 + 67 248 169 195 242 118 128 202 116 39 160 204 197 2 176 15 + 125 194 224 108 176 167 227 78 158 198 187 26 201 243 17 75 + 51 163 109 19 106 12 66 105 51 142 148 139 165 224 99 159 + 50 14 172 196 47 108 237 165 215 74 121 141 255 168 79 1 + 187 35 138 199 43 230 107 84 241 120 156 146 235 79 76 191 + 25 49 147 109 5 7 71 134 98 116 10 47 62 251 32 128 + 211 234 40 111 124 26 81 66 90 57 210 213 222 113 32 116 + 225 1 229 113 230 123 136 19 55 103 75 15 175 210 245 184 + 102 210 199 112 188 66 111 148 235 49 165 182 188 252 127 17 + 30 0 156 211 24 101 18 20 221 217 163 59 142 9 54 54 + 21 167 194 223 242 204 123 62 1 9 42 236 28 120 17 21 + 94 206 25 11 88 99 164 145 175 185 89 126 254 51 101 176 + 64 253 240 231 13 45 219 173 76 12 62 173 206 194 251 194 + 3 87 53 184 80 202 11 183 23 145 192 60 98 205 247 250 + 173 84 123 171 51 240 80 246 47 65 0 221 81 249 241 20 + 135 233 109 250 54 121 143 183 56 143 182 214 117 247 106 126 + 65 231 107 62 33 101 245 55 235 96 99 17 22 206 20 32 + 247 38 183 224 208 76 93 113 194 44 82 182 53 144 136 82 + 90 96 117 117 107 62 249 58 22 76 148 131 177 16 158 69 + 38 73 154 170 199 140 251 35 242 67 60 130 172 2 33 36 + 172 0 239 153 157 87 125 156 37 85 8 194 220 5 168 195 + 248 229 76 254 14 106 47 207 119 142 19 25 250 126 138 227 + 236 54 150 68 16 63 11 43 225 195 1 50 12 43 227 249 + 169 86 65 144 105 168 147 249 107 137 140 246 4 32 226 83 + 68 73 93 177 240 64 135 164 109 64 148 152 100 71 175 243 + 241 255 251 75 54 13 184 31 183 99 106 174 119 247 54 123 + 123 139 34 139 95 167 3 192 251 252 46 36 198 45 40 19 + 175 96 179 10 188 207 0 133 9 219 74 76 178 74 118 109 + 154 107 171 246 21 173 42 50 144 26 19 242 176 4 153 143 + 14 202 164 106 85 227 66 219 143 79 59 193 186 3 58 102 + 90 231 136 166 121 56 60 105 114 194 178 205 86 219 0 123 + 186 53 105 223 192 72 94 176 248 77 249 223 50 220 110 41 + 218 8 42 129 138 235 157 170 72 141 107 9 4 54 62 253 + 72 230 244 9 214 154 57 155 210 15 52 19 165 158 179 155 + 186 133 133 166 87 83 142 247 24 69 74 210 158 99 62 201 + 218 64 38 3 64 199 63 133 2 124 55 121 52 7 210 155 + 75 154 31 245 14 168 121 176 227 24 41 24 146 55 31 160 + 139 199 122 60 176 5 214 240 123 229 84 173 172 156 86 71 + 16 164 25 111 33 6 130 187 87 3 235 202 34 168 5 106 + 60 66 34 193 107 133 158 66 112 233 148 59 182 9 45 18 + 66 106 142 161 222 121 195 254 22 19 176 187 15 74 135 235 + 108 191 4 62 18 115 138 24 45 211 254 153 134 72 168 52 + 20 18 124 106 197 122 146 254 222 108 69 221 187 52 26 253 + 67 164 51 113 146 159 99 89 39 191 241 86 92 76 232 208 + 175 236 205 205 171 189 110 56 146 207 3 100 165 238 228 93 + 172 50 107 222 45 1 55 5 139 22 76 77 53 146 7 183 + 77 194 208 246 186 164 128 221 26 143 1 126 203 153 153 198 + 196 243 103 229 28 166 222 219 64 235 137 165 66 197 5 70 + 104 174 200 31 64 240 227 214 125 218 119 37 106 127 78 222 + 81 210 106 65 108 142 129 210 217 51 80 150 159 75 21 9 + 221 188 112 100 103 238 184 133 242 153 210 18 166 220 96 219 + 44 119 75 191 98 64 108 147 179 73 24 251 8 82 71 12 + 211 133 99 25 208 35 30 243 43 86 153 101 57 14 11 197 + 75 223 168 169 124 98 117 253 75 242 209 211 184 218 80 215 + 85 181 247 147 212 33 158 245 51 208 196 17 153 154 175 35 + 44 152 211 206 230 19 124 98 45 244 32 96 37 78 145 254 + 213 202 169 22 7 150 79 28 49 223 163 83 39 181 122 106 + 62 214 17 56 47 3 204 162 252 183 175 226 64 149 22 114 + 172 240 101 154 142 247 21 65 218 236 200 155 251 57 135 86 + 113 34 210 212 198 46 18 215 16 17 41 159 160 74 109 78 + 15 143 32 18 159 221 54 1 127 236 134 239 59 87 65 156 + 74 119 80 170 156 250 181 182 221 248 41 79 230 204 44 79 + 221 199 245 48 224 181 208 132 26 43 43 2 150 164 200 25 + 64 157 199 177 189 225 180 237 9 74 57 84 227 249 201 219 + 106 210 216 139 69 94 130 223 5 69 125 87 43 159 236 156 + 42 174 240 26 70 222 179 210 74 2 5 142 95 57 63 24 + 56 169 84 159 47 254 187 212 254 76 198 216 180 1 215 124 + 109 94 76 71 171 99 107 227 234 204 248 216 135 112 201 80 + 54 145 67 230 235 142 215 233 227 140 216 109 146 82 96 90 + 139 6 76 101 120 153 27 125 165 237 0 203 75 29 27 185 + 20 41 220 250 102 185 167 252 209 88 218 149 65 114 194 241 + 12 92 7 151 121 88 74 168 63 151 237 107 15 255 208 35 + 98 98 121 17 51 165 181 157 90 81 148 222 114 155 187 194 + 186 33 6 142 198 210 154 94 229 40 213 122 217 147 107 14 + 73 92 60 3 10 166 78 83 60 200 59 220 194 245 17 67 + 19 216 251 87 184 62 221 160 118 170 11 203 186 173 7 136 + 23 135 88 243 145 134 92 136 181 160 44 91 13 175 90 98 + 65 0 202 113 41 180 109 210 245 128 133 20 139 98 57 247 + 27 90 139 5 212 139 73 1 22 155 14 143 82 156 251 248 + 174 124 155 16 234 163 198 10 80 192 194 48 124 143 1 162 + 85 255 131 134 47 115 160 147 69 149 132 242 13 172 54 198 + 114 36 65 233 76 166 100 183 32 242 173 236 177 110 113 1 + 70 26 37 250 224 138 252 103 58 177 177 23 112 199 215 228 + 189 231 47 211 91 41 29 180 222 166 83 147 175 81 71 95 + 30 242 6 106 126 8 75 89 160 209 208 178 189 172 198 17 + 176 57 39 103 225 157 245 253 227 127 13 116 80 23 194 240 + 247 122 0 186 208 99 53 223 68 105 32 52 246 93 102 149 + 225 18 160 104 146 82 19 225 218 19 249 174 233 120 187 94 + 150 139 132 207 16 188 61 82 36 43 215 82 250 144 199 58 + 182 197 230 142 233 91 85 31 40 125 129 158 51 168 59 67 + 80 41 79 9 77 120 41 173 233 123 17 88 76 220 163 173 + 246 231 148 40 203 62 211 109 151 46 215 133 230 139 164 62 + 245 8 13 244 156 102 241 214 159 221 236 212 21 24 219 191 + 160 12 98 102 219 46 206 39 1 249 212 164 83 123 74 27 + 102 244 142 217 128 17 238 210 140 86 130 253 16 133 194 222 + 167 17 162 152 165 151 61 122 222 56 19 169 68 104 142 79 + 186 114 92 252 54 137 0 136 20 59 75 22 196 219 8 70 + 68 215 90 124 235 19 114 36 140 236 97 55 180 3 240 5 + 219 57 153 254 100 214 89 72 84 13 153 206 5 173 33 37 + 115 16 2 242 50 107 93 57 217 215 57 253 57 56 251 87 + 218 184 210 245 215 224 47 89 11 57 87 156 175 31 135 129 + 99 22 253 199 234 128 231 66 145 228 236 111 184 36 106 3 + 10 206 234 234 4 215 44 79 74 179 54 113 179 80 104 229 + 130 131 251 39 112 72 245 233 144 92 107 89 186 77 217 247 + 189 226 179 227 88 83 3 160 121 145 229 144 42 154 94 26 + 249 252 221 189 129 23 241 96 144 95 1 254 204 36 247 248 + 254 197 142 169 138 167 183 236 99 163 218 177 118 105 35 236 + 24 16 182 185 230 133 175 113 143 242 58 223 96 97 182 29 + 137 19 119 195 138 187 79 133 186 35 194 255 58 102 91 169 + 64 125 125 27 220 100 228 130 110 210 113 79 3 18 35 90 + 235 120 240 205 244 223 161 77 61 207 46 96 66 77 69 235 + 160 103 99 73 234 133 208 147 119 48 59 83 31 245 139 132 + 56 122 212 44 173 18 246 106 194 58 199 34 19 128 142 45 + 126 35 166 6 90 167 180 1 93 118 173 146 212 10 198 132 + 115 197 15 117 149 181 8 119 156 58 92 228 130 148 151 87 + 203 78 244 151 139 227 141 37 128 235 179 33 55 44 206 181 + 172 211 68 21 6 183 222 181 140 191 29 224 20 158 88 77 + 60 36 203 241 36 195 87 165 95 20 47 202 244 42 65 151 + 48 181 61 1 224 6 184 35 163 163 45 54 111 137 12 7 + 22 138 59 192 197 73 21 36 71 19 1 189 110 50 197 159 + 202 44 70 229 36 157 3 87 172 140 152 24 182 75 240 160 + 25 173 103 19 168 139 204 174 218 216 234 171 89 60 255 231 + 229 210 162 99 97 168 229 91 213 135 43 129 28 245 117 129 + 167 107 182 234 203 26 154 10 184 153 219 255 241 39 211 152 + 165 152 202 84 74 217 214 198 146 73 234 12 17 47 172 0 + 33 255 72 121 12 248 169 64 229 112 244 84 54 235 37 222 + 32 63 21 160 46 119 214 179 159 103 246 81 229 176 188 214 + 133 135 177 143 177 106 51 63 205 181 85 50 226 171 138 195 + 157 240 68 9 189 152 239 9 152 163 13 170 9 230 123 70 + 107 92 94 193 251 178 73 19 30 59 241 27 20 172 0 216 + 241 174 196 216 139 51 43 80 20 23 50 73 241 200 66 81 + 245 81 214 87 127 131 10 30 207 109 30 11 34 46 38 62 + 87 69 5 172 62 106 132 242 203 104 167 192 70 211 102 50 + 156 228 16 134 103 121 10 163 101 57 138 15 14 62 219 115 + 79 229 227 193 140 127 219 153 22 144 77 151 183 96 238 81 + 40 188 95 213 31 141 146 112 84 34 222 89 163 36 234 121 + 235 33 9 11 214 106 52 177 152 202 149 51 47 69 254 158 + 178 59 29 136 33 136 253 35 108 226 129 154 30 41 29 41 + 31 193 156 76 87 175 206 143 147 211 156 5 175 246 215 247 + 17 197 213 125 177 1 14 249 252 221 229 141 52 4 76 11 + 232 218 67 144 100 211 160 33 181 236 182 115 221 76 95 147 + 138 151 175 137 17 146 23 205 71 159 120 170 118 72 100 204 + 128 115 60 251 219 59 211 22 156 134 146 184 66 215 233 119 + 185 151 113 91 15 198 32 235 194 101 62 206 169 65 138 117 + 54 218 4 55 16 132 161 57 109 149 141 84 204 227 76 88 + 79 94 145 203 211 192 59 107 74 58 26 80 34 67 97 26 + 110 173 77 38 168 125 108 245 180 49 54 181 92 249 48 36 + 18 167 134 132 98 97 195 144 19 0 171 93 166 208 71 144 + 36 122 2 121 36 169 159 96 24 144 61 241 9 58 204 12 + 122 37 27 150 236 1 221 217 54 227 102 223 31 215 72 35 + 139 247 219 24 47 155 64 194 151 176 94 8 14 0 172 244 + 171 110 184 131 179 189 148 189 201 54 110 196 189 138 147 193 + 206 203 116 152 185 164 175 158 44 199 29 165 67 137 102 98 + 238 246 136 253 159 73 214 3 33 92 39 54 166 133 176 191 + 49 201 14 209 94 202 149 73 90 228 143 72 39 77 0 122 + 163 60 144 169 78 123 99 243 176 54 85 190 170 253 143 63 + 59 57 96 41 223 245 69 252 244 0 25 202 147 120 155 1 + 67 170 114 72 46 241 60 101 188 79 119 106 225 195 7 53 + 64 157 7 7 216 47 211 77 109 108 215 224 9 142 112 242 + 157 206 87 243 174 5 209 5 125 69 170 184 70 176 53 172 + 129 167 17 10 235 241 189 201 228 88 13 153 210 170 189 16 + 60 211 43 191 224 34 195 224 81 68 29 234 127 70 179 167 + 54 247 136 4 109 127 114 76 246 54 101 100 243 240 65 71 + 95 39 112 191 25 170 200 176 198 101 50 174 58 243 72 103 + 245 31 251 48 78 24 18 139 184 30 180 70 101 13 44 95 + 243 135 146 105 219 7 40 155 58 250 162 178 64 35 148 24 + 108 248 46 33 6 9 19 82 188 68 176 30 46 26 54 110 + 70 143 30 7 56 164 230 251 64 101 130 131 63 209 89 29 + 46 144 98 205 141 147 241 230 158 128 93 213 116 156 100 178 + 120 236 201 11 18 207 78 68 207 74 149 94 40 27 165 228 + 6 192 51 160 203 40 217 59 66 136 107 51 252 26 90 125 + 156 54 100 152 88 181 13 149 55 244 198 78 154 149 223 81 + 93 41 197 251 56 20 190 152 240 74 93 159 152 187 203 188 + 227 54 173 211 223 73 44 19 103 169 232 77 110 139 181 75 + 38 62 173 24 192 133 240 215 41 28 225 206 25 161 82 246 + 45 120 41 212 133 2 140 55 63 130 195 25 254 252 244 178 + 192 35 16 95 149 91 23 210 40 166 17 47 81 207 236 239 + 9 43 40 90 90 87 180 199 23 55 140 173 196 166 113 188 + 53 86 56 170 105 162 234 139 166 240 35 126 151 221 133 183 + 238 221 139 134 147 169 32 22 123 2 149 158 36 255 90 182 + 132 230 240 245 40 39 205 45 6 238 93 233 71 27 118 6 + 161 104 57 201 238 15 135 60 14 123 137 226 131 198 201 192 + 101 206 252 205 8 164 109 86 55 176 255 116 97 176 178 173 + 217 186 178 209 226 100 63 144 181 55 215 216 112 177 52 220 + 215 179 247 157 40 173 106 187 114 81 156 84 251 104 240 42 + 206 148 250 152 254 202 241 255 4 153 229 141 117 108 255 242 + 240 114 58 73 82 34 195 158 84 60 150 201 79 126 226 119 + 89 17 230 19 40 155 3 45 205 214 22 46 10 2 185 94 + 80 162 42 75 29 147 240 110 64 16 78 63 206 181 198 67 + 13 74 14 128 79 39 137 251 145 236 117 11 122 164 234 236 + 161 217 84 190 106 245 75 220 50 83 188 236 77 67 63 113 + 179 140 251 63 35 164 249 230 53 168 101 151 125 113 124 61 + 147 100 119 58 113 171 72 138 2 119 181 233 105 216 238 178 + 80 51 25 81 16 71 130 4 9 168 16 75 237 127 243 195 + 30 175 34 195 33 115 222 200 128 77 251 11 238 26 110 160 + 86 15 160 133 138 108 27 37 115 1 150 58 90 254 119 111 + 236 4 204 168 51 12 184 46 34 67 62 150 83 137 205 116 + 85 163 147 117 83 78 85 214 209 192 46 118 157 126 224 240 + 213 229 138 3 86 144 53 35 41 194 179 230 5 27 252 11 + 50 92 200 122 14 130 156 184 109 75 39 36 59 173 6 255 + 214 239 100 80 31 251 221 244 252 147 235 33 230 229 216 172 + 121 186 2 251 15 95 189 31 64 28 232 134 41 183 229 76 + 30 168 34 109 114 80 80 225 183 238 151 74 51 150 31 63 + 66 169 22 158 221 243 218 199 134 197 245 136 80 80 179 200 + 69 134 160 106 135 206 155 166 201 70 161 116 112 107 75 154 + 206 158 181 144 239 81 68 148 80 61 134 39 171 100 11 195 + 155 18 68 237 140 20 115 109 192 153 144 90 22 154 103 96 + 239 165 133 176 184 244 96 245 222 71 72 116 144 15 22 25 + 59 191 33 101 66 5 5 82 126 137 71 5 3 139 112 153 + 247 20 24 239 105 177 190 244 42 42 210 199 240 108 72 224 + 210 134 242 30 26 142 163 228 224 192 8 119 208 96 133 133 + 220 139 27 141 12 143 192 197 219 36 13 254 14 8 46 157 + 10 63 122 199 199 79 65 47 135 251 207 80 196 91 157 229 + 112 52 97 178 173 244 75 6 158 44 166 59 126 202 34 74 + 187 234 89 63 71 113 81 27 206 32 97 45 32 27 211 243 + 80 187 1 97 152 114 203 45 6 212 252 2 187 226 75 177 + 26 71 242 41 86 80 242 247 94 212 30 62 7 170 115 229 + 224 13 108 32 122 78 251 116 222 189 74 236 141 194 98 75 + 255 0 174 117 219 1 89 219 109 243 213 195 18 226 185 230 + 53 250 17 31 57 12 82 13 247 69 61 203 133 215 129 247 + 179 50 43 176 92 52 142 211 11 4 85 64 186 201 14 72 + 45 144 84 241 86 100 34 138 146 216 133 218 153 234 218 160 + 109 115 34 98 54 44 17 144 81 244 13 28 140 36 123 103 + 76 194 47 127 212 12 153 121 251 6 214 31 148 73 3 242 + 59 202 74 235 77 104 59 50 220 95 135 132 63 209 205 119 + 246 56 2 217 6 248 206 41 28 29 136 29 63 75 227 231 + 99 244 176 224 58 245 218 14 62 165 9 99 116 54 221 180 + 113 216 30 199 89 21 43 54 3 206 203 225 185 223 2 101 + 9 49 45 133 25 249 50 158 220 53 182 54 118 110 109 16 + 103 105 137 5 40 54 247 10 210 191 138 176 40 121 246 106 + 243 14 160 149 72 128 92 189 175 153 159 51 204 220 96 35 + 123 17 163 192 64 111 197 214 62 151 109 217 48 147 49 123 + 125 91 229 234 8 8 186 166 245 187 48 187 101 132 169 73 + 118 39 118 207 241 38 214 129 249 77 88 25 0 173 183 48 + 240 179 186 56 16 216 226 95 132 176 176 11 69 43 35 23 + 43 139 93 234 151 136 132 100 220 8 85 113 93 233 113 1 + 13 118 153 45 95 99 249 1 29 89 192 8 102 178 143 235 + 163 144 180 112 45 152 50 251 56 23 94 100 225 48 22 148 + 205 252 4 249 173 62 142 52 55 217 155 93 81 209 34 62 + 247 147 212 37 141 65 104 22 227 101 205 119 47 80 253 215 + 225 255 206 163 50 87 209 39 3 180 209 151 13 42 39 194 + 182 198 144 219 21 0 227 70 163 120 44 4 98 239 118 137 + 19 203 207 192 180 70 44 67 255 42 22 201 241 173 122 250` +) + +func Test_marshalAndCompressDataSplitsBatches(t *testing.T) { + type args struct { + d *Dispatcher + data []map[string]interface{} + dataType string + } + tests := []struct { + name string + args args + wantLen int + wantErr bool + }{ + { + name: "control", + args: args{ + d: &Dispatcher{ + compressTool: util.NewCompressTool(), + }, + data: []map[string]interface{}{ + { + "control": "this should marshal correctly", + }, + }, + dataType: "control", + }, + wantLen: 1, + }, + { + name: "should split", + args: args{ + d: &Dispatcher{ + compressTool: util.NewCompressTool(), + }, + data: []map[string]interface{}{ + { + "one": kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte, + }, + { + "two": kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte, + }, + }, + dataType: "control", + }, + wantLen: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := marshalAndCompressData(tt.args.d, tt.args.data, tt.args.dataType) + fmt.Printf("payload is %d bytes\n", got[0].Len()) + if (err != nil) != tt.wantErr { + t.Errorf("marshalAndCompressData() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(got) != tt.wantLen { + t.Errorf("did not properly split batches: wanted %d, got %d", tt.wantLen, len(got)) + } + + for _, payload := range got { + if payload.Len() > maxPayloadSizeBytes { + t.Errorf("Payload of len %d is greater than maximum %d", payload.Len(), maxPayloadSizeBytes) + } + } + }) + } +} + +// Note that this is very expensive in memory and execution time +// This is a good candidate for optimization +// The recursion could blow the memory stack if payloads are big enough +// since the recursion splits the payloads in half, the memory needed is n * log(n) +// where n is the size of the initial payload +func BenchmarkMarshalAndCompressDataWithSplit(b *testing.B) { + d := &Dispatcher{ + compressTool: util.NewCompressTool(), + } + data := []map[string]interface{}{ + { + "one": kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte, + }, + { + "two": kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte + kilobyte, + }, + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + marshalAndCompressData(d, data, "benchmark") + } +} diff --git a/util/close.go b/util/close.go index f6dbeafe..ba6c01cd 100644 --- a/util/close.go +++ b/util/close.go @@ -2,12 +2,16 @@ package util import ( "io" + + log "github.com/sirupsen/logrus" ) +var l = log.WithFields(log.Fields{"pkg": "util"}) + // Close closes things and logs errors if it fails func Close(thing io.Closer) { err := thing.Close() if err != nil { - Logln(err) + l.Error(err) } } diff --git a/util/compress.go b/util/compress.go index 9c03b826..3fa2d221 100644 --- a/util/compress.go +++ b/util/compress.go @@ -3,21 +3,40 @@ package util import ( "bytes" "compress/gzip" - "io/ioutil" + "io" + "sync" ) +type CompressTool struct { + writers *sync.Pool +} + +func NewCompressTool() *CompressTool { + return &CompressTool{ + writers: &sync.Pool{ + New: func() any { + return gzip.NewWriter(io.Discard) + }, + }, + } +} + // Compress gzips the given input. -func Compress(b []byte) (*bytes.Buffer, error) { +func (ct *CompressTool) Compress(b []byte) (*bytes.Buffer, error) { var buf bytes.Buffer - w := gzip.NewWriter(&buf) + w := ct.writers.Get().(*gzip.Writer) + defer func() { + Close(w) + ct.writers.Put(w) + }() + + w.Reset(&buf) _, err := w.Write(b) if err != nil { return nil, err } - defer Close(w) - return &buf, nil } @@ -32,5 +51,5 @@ func Uncompress(b []byte) ([]byte, error) { defer Close(gz) - return ioutil.ReadAll(gz) + return io.ReadAll(gz) } diff --git a/util/compress_test.go b/util/compress_test.go index 0e37c06f..3595360d 100644 --- a/util/compress_test.go +++ b/util/compress_test.go @@ -1,13 +1,51 @@ package util import ( + "bytes" + "compress/gzip" "testing" "github.com/stretchr/testify/assert" ) +var ( + benchmarkSlice = []byte("asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf") +) + +func ControlCompress(b []byte) (*bytes.Buffer, error) { + var buf bytes.Buffer + + w := gzip.NewWriter(&buf) + _, err := w.Write(b) + if err != nil { + return nil, err + } + + defer Close(w) + return &buf, nil +} + +func BenchmarkControl(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + ControlCompress(benchmarkSlice) + } +} + +func BenchmarkCompressTool(b *testing.B) { + ct := NewCompressTool() + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + ct.Compress(benchmarkSlice) + } +} + func TestCompress(t *testing.T) { - b, err := Compress([]byte("foobar")) + ct := NewCompressTool() + b, err := ct.Compress([]byte("foobar")) assert.Nil(t, err) assert.NotEmpty(t, b) } @@ -16,7 +54,8 @@ func TestUncompress(t *testing.T) { b, err := Uncompress([]byte("foobar")) assert.Error(t, err) - c, err := Compress([]byte("foobar")) + ct := NewCompressTool() + c, err := ct.Compress([]byte("foobar")) assert.Nil(t, err) b, err = Uncompress(c.Bytes()) diff --git a/util/extension.go b/util/extension.go index 0f732276..05cd15fd 100644 --- a/util/extension.go +++ b/util/extension.go @@ -1,7 +1,14 @@ package util +import "time" + const ( Name = "newrelic-lambda-extension" - Version = "2.3.6" + Version = "3.0.0" Id = Name + ":" + Version ) + +var ( + //TODO: make this much lower once collector repaired + SendToNewRelicTimeout = 2400 * time.Millisecond +) diff --git a/util/logger.go b/util/logger.go deleted file mode 100644 index 0c4d3ff6..00000000 --- a/util/logger.go +++ /dev/null @@ -1,84 +0,0 @@ -package util - -import "log" - -var logger = Logger{ - isEnabled: true, - isDebugEnabled: false, -} - -type Logger struct { - isEnabled bool - isDebugEnabled bool -} - -func ConfigLogger(logsEnabled bool, isDebugEnabled bool) { - // Go Logging config - log.SetPrefix("[NR_EXT] ") - log.SetFlags(0) - - log.Println("New Relic Lambda Extension starting up") - - logger.isEnabled = logsEnabled - logger.isDebugEnabled = isDebugEnabled -} - -func (l Logger) Debugf(format string, v ...interface{}) { - if l.isEnabled && l.isDebugEnabled { - log.Printf(format, v...) - } -} - -func (l Logger) Debugln(v ...interface{}) { - if l.isEnabled && l.isDebugEnabled { - log.Println(v...) - } -} - -func (l Logger) Logf(format string, v ...interface{}) { - if l.isEnabled { - log.Printf(format, v...) - } -} - -func (l Logger) Logln(v ...interface{}) { - if l.isEnabled { - log.Println(v...) - } -} - -func Debugf(format string, v ...interface{}) { - if logger.isEnabled && logger.isDebugEnabled { - log.Printf(format, v...) - } -} - -func Debugln(v ...interface{}) { - if logger.isEnabled && logger.isDebugEnabled { - log.Println(v...) - } -} - -func Logf(format string, v ...interface{}) { - if logger.isEnabled { - log.Printf(format, v...) - } -} - -func Logln(v ...interface{}) { - if logger.isEnabled { - log.Println(v...) - } -} - -func Fatal(v ...interface{}) { - if logger.isEnabled { - log.Fatal(v...) - } -} - -func Panic(v ...interface{}) { - if logger.isEnabled { - log.Panic(v...) - } -}