Instrumenting .NET AWS Lambda Functions with net_profiler_rust


In the first net_profiler_rust article I used the profiler with local .NET applications. The same approach also works with AWS Lambda: a Lambda layer installs the profiler and starts OpenTelemetry without adding OpenTelemetry code or packages to the function.

The layer supports the managed dotnet8 and dotnet10 Lambda runtimes on ARM64 and x86_64. This example uses .NET 10 and ARM64 because the current AWS SDK instrumentation is included in the .NET 10 agent package.

Create a DynamoDB Lambda

Install the AWS Lambda templates and deployment tool:

dotnet new install Amazon.Lambda.Templates
dotnet tool install -g Amazon.Lambda.Tools

dotnet new lambda.EmptyFunction -n LambdaProfilerDemo
cd LambdaProfilerDemo/src/LambdaProfilerDemo
dotnet add package AWSSDK.DynamoDBv2

Change the project target framework to net10.0, then use this handler:

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda.Core;
using Amazon.Lambda.Serialization.SystemTextJson;

[assembly: LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]

namespace LambdaProfilerDemo;

public sealed record CalculatorInput(string Id, int Left, int Right);
public sealed record CalculatorResult(string Id, int Left, int Right, int Result);

public sealed class Function
{
    private readonly IAmazonDynamoDB dynamoDb = new AmazonDynamoDBClient();
    private readonly string tableName =
        Environment.GetEnvironmentVariable("TABLE_NAME") ?? "lambda-profiler-demo";

    public async Task<CalculatorResult> FunctionHandler(
        CalculatorInput input, ILambdaContext context)
    {
        var result = input.Left / input.Right;

        await dynamoDb.PutItemAsync(new PutItemRequest
        {
            TableName = tableName,
            Item = new()
            {
                ["id"] = new() { S = input.Id },
                ["left"] = new() { N = input.Left.ToString() },
                ["right"] = new() { N = input.Right.ToString() },
                ["result"] = new() { N = result.ToString() }
            }
        });

        var stored = await dynamoDb.GetItemAsync(new GetItemRequest
        {
            TableName = tableName,
            Key = new() { ["id"] = new() { S = input.Id } },
            ConsistentRead = true
        });

        return new(
            input.Id,
            int.Parse(stored.Item["left"].N),
            int.Parse(stored.Item["right"].N),
            int.Parse(stored.Item["result"].N));
    }
}

Create the table:

aws dynamodb create-table \
  --region eu-central-1 \
  --table-name lambda-profiler-demo \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

The Lambda execution role needs the normal CloudWatch Logs permissions plus dynamodb:PutItem and dynamodb:GetItem for this table. Deploy the function as ARM64:

dotnet lambda deploy-function lambda-profiler-demo \
  --region eu-central-1 \
  --framework net10.0 \
  --function-runtime dotnet10 \
  --function-architecture arm64 \
  --function-handler LambdaProfilerDemo::LambdaProfilerDemo.Function::FunctionHandler \
  --function-role <lambda-role-arn> \
  --function-timeout 30 \
  --function-memory-size 512 \
  --environment-variables TABLE_NAME=lambda-profiler-demo

Download and configure the profiler layer

Open the net_profiler_rust download page, select the latest release, and open its lambda-layers directory.

Download the package matching the function architecture:

This example uses the ARM64 package. Extract it and enter the package directory:

unzip profiler-lambda-layer-<version>-linux-arm64-package.zip
cd profiler-lambda-layer-<version>-linux-arm64

The downloaded package contains:

Edit layer/profiler/settings/lambda/settings.yaml for Dynatrace:

General:
  Enabled: true
  Provider: dynatrace
  Type: awslambda

Connection:
  EndpointUrl: https://<environment>/api/v2/otlp
  Type: http
  BearerToken: <token>

Monitoring:
  ServiceName: lambda-profiler-demo
  Enabled: true
  Special:
    - AWS
  Metrics:
    ServiceName: lambda-profiler-demo
    Enabled: false
    CollectionInterval: 10
    Cpu: false
    Memory: false

Recreate the layer ZIP so it contains the modified settings:

LAYER_ZIP="$(find . -maxdepth 1 -name 'profiler-lambda-layer-*-linux-arm64.zip' -print -quit)"
rm "$LAYER_ZIP"
(cd layer && zip -qr "../$LAYER_ZIP" .)

The layer installs the startup wrapper, native profiler, managed agent assemblies, and settings under /opt. The Dynatrace token is now embedded in this private layer, so do not publish the ZIP or layer publicly.

Publish and attach the layer

The included deployment script publishes the layer, attaches it to the function, and merges the required profiler environment variables:

chmod +x deploy-layer.sh
./deploy-layer.sh net-profiler-rust lambda-profiler-demo eu-central-1

The script reads the layer ZIP and function-environment.json from the extracted package. On a later deployment, remove an obsolete version of this layer from the function if it is still attached.

For an x86_64 Lambda, download the linux-x64-package.zip instead. Its deployment script publishes the layer with the matching architecture.

AWS Lambda configuration showing the attached net_profiler_rust layer
The profiler layer attached to the ARM64 Lambda.
CloudWatch logs showing net_profiler_rust wrapper startup diagnostics
CloudWatch startup diagnostics from profiler_wrapper.

Invoke and verify

Invoke the function with an explicit payload:

aws lambda invoke \
  --region eu-central-1 \
  --function-name lambda-profiler-demo \
  --cli-binary-format raw-in-base64-out \
  --payload '{"id":"article-demo","left":84,"right":2}' \
  response.json

cat response.json

CloudWatch should contain lines prefixed with [profiler_wrapper]. In Dynatrace, the Lambda invocation appears as a server span with the function name, version, invocation ID, ARN, region, and trigger type. PutItem and GetItem appear as child spans. The agent flushes traces and metrics before the invocation returns.

Dynatrace distributed trace showing an AWS Lambda invocation and DynamoDB child spans
Verified trace structure exported to Dynatrace.
Dynatrace span details showing AWS Lambda and DynamoDB attributes
Lambda and DynamoDB attributes from the verified invocation.

Supported AWS instrumentation

There are two separate types of instrumentation:

HttpClient and gRPC client tracing can also be enabled in Monitoring.Special. AWS SDK tracing currently uses OpenTelemetry.Instrumentation.AWS 1.11.0. Its semantic conventions are still beta, and the service list above is illustrative rather than an exhaustive compatibility guarantee.

Future: Azure Functions

Azure Functions support is planned, beginning with the .NET isolated worker model. The intended direction includes automatic invocation spans, trigger classification, Azure SDK client instrumentation, OTLP export, end-of-invocation flushing, and a deployment package comparable to the AWS Lambda layer workflow. There is no release date yet.