Skill 46 · Setting Up CloudWatch Observability
Subchapter 46.25
references/cloudwatch-omni/instrumentation/lambda-dotnet.mdMarkdown10 KBView on GitHub
Add the ADOT .NET Lambda layer to a function so it emits OpenTelemetry traces, with Application Signals explicitly disabled.
Read first. Note that : the Lambda execution environment supplies its own X-Ray receiver, and with active tracing enabled the layer exports traces to it — so the function’s export path is already wired rather than left at the SDK default. That is why this guide keeps the layer’s standard configuration instead of omitting endpoint setup entirely.
If you cannot determine a value (such as the AWS Region), ask the user. Do not guess.
Do NOT:
OTEL_AWS_APPLICATION_SIGNALS_ENABLED=true, or add OTEL_AWS_SERVICE_EVENTS_* / OTEL_AWS_DYNAMIC_INSTRUMENTATION_* variablesCloudWatchLambdaApplicationSignalsExecutionRolePolicy — it grants Application Signals, which is disabled hereCORECLR_* / DOTNET_* variables by hand — the layer’s wrapper configures the profilerterraform apply, cdk deploy, or sam deploy automatically.csprojThe ADOT Lambda layer ARN is region-specific and its layer version changes over time. Look up the current value rather than copying one from this guide:
ARN format — fill in <REGION>, <ACCOUNT_ID>, and <LAYER_VERSION> from the source above:
arn:aws:lambda:<REGION>:<ACCOUNT_ID>:layer:AWSOpenTelemetryDistroDotNet:<LAYER_VERSION>Sample regions (illustrative — confirm the current version and account ID, and use the source of truth for any supported region, not just these):
us-east-1: arn:aws:lambda:us-east-1:615299751070:layer:AWSOpenTelemetryDistroDotNet:<LAYER_VERSION>
us-west-2: arn:aws:lambda:us-west-2:615299751070:layer:AWSOpenTelemetryDistroDotNet:<LAYER_VERSION>
ap-east-1: arn:aws:lambda:ap-east-1:888577020596:layer:AWSOpenTelemetryDistroDotNet:<LAYER_VERSION>
eu-south-1: arn:aws:lambda:eu-south-1:257394471194:layer:AWSOpenTelemetryDistroDotNet:<LAYER_VERSION>
...Some partitions use a different prefix and account ID (
arn:aws-cn:for China). The source of truth has the exact ARN per region.
CDK:
// lambda-stack.ts
const myFunction = new lambda.Function(this, 'MyFunction', {
// ... existing runtime, handler, code unchanged ...
layers: [
lambda.LayerVersion.fromLayerVersionArn(this, 'AdotLayer', adotLayerArn),
],
});Terraform:
# lambda.tf
resource "aws_lambda_function" "my_function" {
# ... existing configuration unchanged ...
layers = [local.adot_layer_arn]
}Layers apply to .zip deployment packages. If the function is packaged as a container image, layers are not available — instrument the image instead using the pattern in ecs-dotnet.md (the same ADOT .NET SDK and CoreCLR variables), and tell the user that the image has to be rebuilt.
Retained from the standard ADOT Lambda setup — active tracing is what makes the execution environment’s X-Ray receiver available, so the layer’s export path depends on it.
CDK: tracing: lambda.Tracing.ACTIVE
Terraform: tracing_config { mode = "Active" }
AWS_LAMBDA_EXEC_WRAPPER = /opt/otel-instrument
OTEL_AWS_APPLICATION_SIGNALS_ENABLED = false
OTEL_SERVICE_NAME = <function-name>AWS_LAMBDA_EXEC_WRAPPER is what loads the SDK — without it the layer is inert. On .NET it is also what sets up the CoreCLR profiler variables, so do not write those by hand. OTEL_AWS_APPLICATION_SIGNALS_ENABLED=false is required, not stylistic. The layer’s otel-instrument wrapper defaults this variable to true when it is unset — every ADOT Lambda runtime does — so taking the defaults would silently ENABLE Application Signals, which is out of scope here. This is the one place in this reference where an OTEL_AWS_* variable is written deliberately, and it must not be omitted.
OTEL_SERVICE_NAME is optional: the wrapper already defaults it to $AWS_LAMBDA_FUNCTION_NAME. Set it only if the service should be named something other than the function. OTEL_METRICS_EXPORTER and OTEL_PROPAGATORS are also defaulted sensibly by the wrapper, so do not set them.
CDK:
environment: {
AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-instrument',
OTEL_AWS_APPLICATION_SIGNALS_ENABLED: 'false',
OTEL_SERVICE_NAME: 'my-function',
},Terraform:
environment {
variables = {
AWS_LAMBDA_EXEC_WRAPPER = "/opt/otel-instrument"
OTEL_AWS_APPLICATION_SIGNALS_ENABLED = "false"
OTEL_SERVICE_NAME = "my-function"
}
}Preserve any environment variables the function already sets — append to them.
With active tracing and the layer’s default configuration, traces reach X-Ray through the execution environment’s X-Ray receiver, so the execution role needs xray:PutTraceSegments and xray:PutTelemetryRecords. AWSLambdaBasicExecutionRole does not include them.
CDK — already handled by Step 2. Setting tracing to anything other than DISABLED makes the Function construct add those two actions to the role as an inline statement. Do not attach AWSXRayDaemonWriteAccess on top of that; confirm the grant with cdk diff instead.
Terraform:
# lambda.tf
resource "aws_iam_role_policy_attachment" "lambda_xray" {
role = aws_iam_role.lambda_exec.name
policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
}SAM: add AWSXRayDaemonWriteAccess to the function’s Policies list. CloudFormation, or a console-managed function: attach the same managed policy to the execution role.
AWSXRayDaemonWriteAccess is broader than what the function actually uses — it also permits sampling-rule reads. If the user prefers least privilege, an inline policy with just the two xray:Put* actions covers this configuration.
The X-Ray permissions in Step 4 are the only ones this instrumentation needs, and they are added here because they follow directly from active tracing on the function itself.
Do not add CloudWatchLambdaApplicationSignalsExecutionRolePolicy — it exists to grant Application Signals, which this configuration disables.
If the function is later pointed at a different destination — such as a customer-run collector — the permissions for that destination are not decided here.
This runtime behaves differently from the other three, and the difference is silent.
The .NET layer’s otel-instrument wrapper contains:
if [ -z "${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}" ] && [ -z "${OTEL_EXPORTER_OTLP_ENDPOINT}" ]; then
export OTEL_TRACES_EXPORTER="none";
fiSo when no OTLP endpoint is configured — which is exactly the state this guide leaves the function in — the wrapper forces the traces exporter off. Instrumentation still loads and the profiler still attaches; spans are simply created and dropped. The function looks healthy and logs nothing unusual.
The other runtimes do not do this: the Python and Node.js wrappers leave
OTEL_TRACES_EXPORTER alone, and the Java wrapper only defaults it
(${OTEL_TRACES_EXPORTER:-"otlp"}), so those three attempt to export and surface
connection errors instead.
Consequence: for .NET, “instrumentation applied” does not mean “traces flowing”. It
starts working once an OTLP endpoint exists and OTEL_EXPORTER_OTLP_ENDPOINT (or
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) points at it. Say this to the
user explicitly, so a silent absence of traces is not mistaken for a broken setup.
Invoke the function and check its CloudWatch Logs. The layer logs that the ADOT/OpenTelemetry instrumentation initialized on cold start.
If nothing is logged, the usual cause is a missing or misspelled AWS_LAMBDA_EXEC_WRAPPER — the profiler then never attaches and the function runs normally with no instrumentation and no error.
Note that a not a dynamic executable line at INIT_START is harmless — it is ldd output
from the wrapper’s glibc/musl detection, not a failure.
Tell the user:
“I’ve added ADOT .NET instrumentation to your Lambda function.
Changes:
AWSOpenTelemetryDistroDotNet layer (version looked up from the ADOT source of truth for your region)AWS_LAMBDA_EXEC_WRAPPER, OTEL_SERVICE_NAME, and OTEL_AWS_APPLICATION_SIGNALS_ENABLED=falsetracing setting, or the AWSXRayDaemonWriteAccess managed policy for Terraform/SAM/CloudFormationNot changed: your handler source and .csproj. The CoreCLR profiler variables are configured by the layer’s wrapper, so none were written into your IaC, and no Application Signals policy was added.
Next steps:
OTEL_TRACES_EXPORTER=none while no OTLP endpoint is set, so spans are created and dropped rather than exported. Configuring a destination — such as a collector — is what turns traces on, and its permissions differ from the X-Ray ones added here.Let me know if you’d like adjustments before you deploy.”