Skip to content

Commit

Permalink
Introduce support for Ruby
Browse files Browse the repository at this point in the history
- Introduce runtime configuration values for Ruby (these do not yet
  power any automated checks)
- Add sam and terraform examples (these are pretty much straight ports
  of the Python examples)
  • Loading branch information
James Bunch committed Mar 28, 2024
1 parent 8200a25 commit 29b9b28
Show file tree
Hide file tree
Showing 15 changed files with 475 additions and 0 deletions.
14 changes: 14 additions & 0 deletions checks/runtime_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ var (
"/opt/python/lib/python3.8/site-packages/newrelic",
"/opt/python/lib/python3.9/site-packages/newrelic",
}
layerAgentPathRuby = []string{"/opt/ruby/gems/3.2.0/gems/newrelic_rpm"}
vendorAgentPathNode = "/var/task/node_modules/newrelic"
vendorAgentPathPython = "/var/task/newrelic"
vendorAgentPathRuby = "/var/task/vendor/bundle/ruby/3.2.0/gems/newrelic_rpm"
runtimeLookupPath = "/var/lang/bin"
)

Expand All @@ -31,6 +33,7 @@ type Runtime string
const (
Python Runtime = "python"
Node Runtime = "node"
Ruby Runtime = "ruby"
)

// Runtime static values
Expand All @@ -55,4 +58,15 @@ var runtimeConfigs = map[Runtime]runtimeConfig{
agentVersionGitOrg: "newrelic",
agentVersionGitRepo: "newrelic-python-agent",
},
Ruby: {
language: Ruby,
wrapperName: "newrelic_lambda_wrapper.handler",
fileType: "rb",
layerAgentPaths: layerAgentPathRuby,
vendorAgentPath: vendorAgentPathRuby,
// TODO: requires Ruby to parse out the version
agentVersionFile: "lib/new_relic/version.rb",
agentVersionGitOrg: "newrelic",
agentVersionGitRepo: "newrelic-ruby-agent",
},
}
64 changes: 64 additions & 0 deletions examples/sam/ruby/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Instrumented Ruby Lambda

This is a "Hello, World" style Lambda function in Ruby, instrumented
with the New Relic agent.

This example is both instructive and a diagnostic tool: if you can
deploy this Lambda function and see its events in NR One, you'll
know that all the telemetry plumbing is connected correctly.

## Building and deploying

### Prerequisites

- The [AWS CLI v2](https://aws.amazon.com/cli/)
- [Docker](https://docs.docker.com/get-docker/)
- The [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)

Make sure you've run the `newrelic-lambda integrations install` command in your
AWS Region, and included the `--enable-license-key-secret` flag.

### deploy script

From a command prompt, in this directory, run

./deploy.sh <accountId> <region>

where `<accountId>` is your New Relic account ID, and `<region>`
is your AWS Region, like "us-west-2".

This will package and deploy the CloudFormation stack for this example
function.

At this point, you can invoke the function. As provided, the example
function doesn't pay attention to its invocation event. If everything
has gone well, each invocation gets reported to New Relic, and its
telemetry appears in NR One.

## Code Structure

Now is also a good time to look at the structure of the example code.

### template.yaml

This function is deployed using a SAM template, which is a CloudFormation
template with some extra syntactic sugar for Lambda functions. In it, we
tell CloudFormation where to find lambda function code, what layers to use, and
what IAM policies to add to the Lambda function's execution role. We also set
environment variables that are available to the handler function.

### app.rb

Lambda functions written in Ruby are involve a Ruby method at a mininum and can
optionally be found within a class and/or module based namespace. The runtime
loads the Ruby code, and then invokes the handler function method for each
invocation event. New Relic publishes a Lambda Layer that wraps your handler
function and initializes the New Relic agent, allowing us to collect telemetry.

There are a couple examples here of how you might add custom events and attributes
to the default telemetry.

Since Ruby is a dynamic, interpreted language, the Agent can inject instrumentation
into the various client libraries you might be using in your function. This happens
once, during cold start, and provides rich, detailed instrumentation out of the box,
with minimal developer effort.
21 changes: 21 additions & 0 deletions examples/sam/ruby/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash

accountId=$1
region=$2

echo "region set to ${region}"

sam build --use-container

bucket="newrelic-example-${region}-${accountId}"

aws s3 mb --region "${region}" "s3://${bucket}"

sam package --region "${region}" --s3-bucket "${bucket}" --output-template-file packaged.yaml

aws cloudformation deploy \
--region "${region}" \
--template-file packaged.yaml \
--stack-name NewrelicExamplePython \
--capabilities CAPABILITY_IAM \
--parameter-overrides "NRAccountId=${accountId}"
62 changes: 62 additions & 0 deletions examples/sam/ruby/events/event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"body": "{\"message\": \"hello world\"}",
"resource": "/{proxy+}",
"path": "/path/to/resource",
"httpMethod": "POST",
"isBase64Encoded": false,
"queryStringParameters": {
"foo": "bar"
},
"pathParameters": {
"proxy": "/path/to/resource"
},
"stageVariables": {
"baz": "qux"
},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, sdch",
"Accept-Language": "en-US,en;q=0.8",
"Cache-Control": "max-age=0",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "US",
"Host": "1234567890.execute-api.us-east-1.amazonaws.com",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Custom User Agent String",
"Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
"X-Forwarded-For": "127.0.0.1, 127.0.0.2",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"requestContext": {
"accountId": "123456789012",
"resourceId": "123456",
"stage": "prod",
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
"requestTime": "09/Apr/2015:12:34:56 +0000",
"requestTimeEpoch": 1428582896000,
"identity": {
"cognitoIdentityPoolId": null,
"accountId": null,
"cognitoIdentityId": null,
"caller": null,
"accessKey": null,
"sourceIp": "127.0.0.1",
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": null,
"userAgent": "Custom User Agent String",
"user": null
},
"path": "/prod/path/to/resource",
"resourcePath": "/{proxy+}",
"httpMethod": "POST",
"apiId": "1234567890",
"protocol": "HTTP/1.1"
}
}
24 changes: 24 additions & 0 deletions examples/sam/ruby/newrelic_example_ruby/app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require 'net/http'
require 'uri'

# Example Ruby Lambda function - module and/or class
# namespacing is optional
class App
def self.lambda_handler(event:, context:)
# instrumentation
uri = URI('https://newrelic.com')
3.times { Net::HTTP.get(uri) }

# custom attributes
# ::NewRelic::Agent.add_custom_attributes(server: 'less', current_time: Time.now.to_s)

# As normal, anything you write to stdout ends up in CloudWatch
puts 'Hello, world'
puts "Event size: #{event.size}"
puts "Context size: #{context.size}"

{ statusCode: 200, body: JSON.generate('Hello from Ruby Lambda!') }
end
end
45 changes: 45 additions & 0 deletions examples/sam/ruby/template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: And example of a simple instrumented Ruby Lambda

Parameters:
NRAccountId:
Type: String
Description: Your New Relic account ID; necessary for distributed tracing.
AllowedPattern: '[0-9]+'

Resources:
NewRelicExample:
Type: AWS::Serverless::Function
Properties:
# In this example, we're using the SAM CLI to package and deploy our lambda. SAM will transform this value during the publish step.
CodeUri: newrelic_example_ruby/
Description: A simple Lambda, with New Relic telemetry
FunctionName: newrelic-example-ruby
# The handler for your function needs to be the one provided by the instrumentation layer, below.
Handler: newrelic_lambda_wrapper.handler
Runtime: ruby3.2
# Currently, we don't support Image based PackageType
PackageType: Zip
Environment:
Variables:
# For the instrumentation handler to invoke your real handler, we need this value
NEW_RELIC_LAMBDA_HANDLER: app.lambda_handler
NEW_RELIC_ACCOUNT_ID: !Sub ${NRAccountId}
# NEW_RELIC_EXTENSION_SEND_FUNCTION_LOGS: true
# NEW_RELIC_EXTENSION_LOG_LEVEL: DEBUG
Layers:
# This layer includes the New Relic Lambda Extension, a sidecar process that sends telemetry,
# as well as the New Relic Agent for Ruby, and a handler wrapper that makes integration easy.
- !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:451483290750:layer:NewRelicRuby32:1
Policies:
# This policy allows the lambda to know the value of the New Relic licence key. We need this so
# that we can send telemetry back to New Relic
- AWSSecretsManagerGetSecretValuePolicy:
SecretArn: !ImportValue NewRelicLicenseKeySecret-NewRelic-LicenseKeySecretARN
Logs:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: {"Fn::Join": ["", ["/aws/lambda/", {"Ref": "NewRelicExample"}]]}
# Lambda functions will auto-create their log group on first execution, but it retains logs forever, which can get expensive.
RetentionInDays: 7
33 changes: 33 additions & 0 deletions examples/sam/ruby/test/handler_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Unit tests
# To run, simply have Ruby v3.2+ and execute this script
# $ ./handler_test.rb

require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'minitest'
end

require 'json'
require 'minitest/autorun'

# Test the Lambda handler
class LambdaHandlerTest < Minitest::Test
EVENT_FILE = '../events/event.json'

def setup
require_relative '../newrelic_example_ruby/app'
end

def test_lambda_handler
event = JSON.parse(File.read(EVENT_FILE))
result = App.lambda_handler(event:, context: {})

assert_kind_of Hash, result, 'Expected a hash result from the Lambda function'
assert_equal 200, result[:statusCode], 'Expected function result to have a 200 status code'
assert_match 'Hello', result[:body], "Expected function result message to match 'Hello'"
end
end
64 changes: 64 additions & 0 deletions examples/terraform/ruby/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Instrumented Ruby Lambda

This is a "Hello, World" style Lambda function in Ruby, instrumented
with the New Relic agent.

This example is both instructive and a diagnostic tool: if you can
deploy this Lambda function and see its events in NR One, you'll
know that all the telemetry plumbing is connected correctly.

## Building and deploying

### Prerequisites

- The [AWS CLI v2](https://aws.amazon.com/cli/)
- [Docker](https://docs.docker.com/get-docker/)
- The [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)

Make sure you've run the `newrelic-lambda integrations install` command in your
AWS Region, and included the `--enable-license-key-secret` flag.

### deploy script

From a command prompt, in this directory, run

./deploy.sh <accountId> <region>

where `<accountId>` is your New Relic account ID, and `<region>`
is your AWS Region, like "us-west-2".

This will package and deploy the CloudFormation stack for this example
function.

At this point, you can invoke the function. As provided, the example
function doesn't pay attention to its invocation event. If everything
has gone well, each invocation gets reported to New Relic, and its
telemetry appears in NR One.

## Code Structure

Now is also a good time to look at the structure of the example code.

### main.tf

This function is deployed using this Terraform script. In it, we
tell Terraform where to find lambda function code, what layers to use, and
what IAM policies to add to the Lambda function's execution role. We also set
environment variables that are available to the handler function.

### app.rb

Lambda functions written in Ruby are involve a Ruby method at a mininum and can
optionally be found within a class and/or module based namespace. The runtime
loads the Ruby code, and then invokes the handler function method for each
invocation event. New Relic publishes a Lambda Layer that wraps your handler
function and initializes the New Relic agent, allowing us to collect telemetry.

There are a couple examples here of how you might add custom events and attributes
to the default telemetry.

Since Ruby is a dynamic, interpreted language, the Agent can inject instrumentation
into the various client libraries you might be using in your function. This happens
once, during cold start, and provides rich, detailed instrumentation out of the box,
with minimal developer effort.

24 changes: 24 additions & 0 deletions examples/terraform/ruby/app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require 'net/http'
require 'uri'

# Example Ruby Lambda function - module and/or class
# namespacing is optional
class App
def self.lambda_handler(event:, context:)
# instrumentation
uri = URI('https://newrelic.com')
3.times { Net::HTTP.get(uri) }

# custom attributes
# ::NewRelic::Agent.add_custom_attributes(server: 'less', current_time: Time.now.to_s)

# As normal, anything you write to stdout ends up in CloudWatch
puts 'Hello, world'
puts "Event size: #{event.size}"
puts "Context size: #{context.size}"

{ statusCode: 200, body: JSON.generate('Hello from Ruby Lambda!') }
end
end
14 changes: 14 additions & 0 deletions examples/terraform/ruby/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

accountId=$1
export TF_VAR_newrelic_account_id=$accountId

region=$2
export TF_VAR_aws_region=$region
echo "region set to ${region}"

rm -f function.zip
zip -rq function.zip app.rb

terraform validate .
terraform apply
Loading

0 comments on commit 29b9b28

Please sign in to comment.